diff options
Diffstat (limited to 'remote/test/puppeteer/test')
273 files changed, 28460 insertions, 0 deletions
diff --git a/remote/test/puppeteer/test/.eslintrc.js b/remote/test/puppeteer/test/.eslintrc.js new file mode 100644 index 0000000000..489868b6ed --- /dev/null +++ b/remote/test/puppeteer/test/.eslintrc.js @@ -0,0 +1,38 @@ +module.exports = { + rules: { + 'no-restricted-imports': [ + 'error', + { + /** The mocha tests run on the compiled output in the /lib directory + * so we should avoid importing from src. + */ + patterns: ['*src*'], + }, + ], + }, + overrides: [ + { + files: ['*.spec.ts'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}, + ], + 'no-restricted-syntax': [ + 'error', + { + message: + 'Use helper command `launch` to make sure the browsers get cleaned', + selector: + 'MemberExpression[object.name="puppeteer"][property.name="launch"]', + }, + { + message: 'Unexpected debugging mocha test.', + selector: + 'CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflake"], CallExpression[callee.object.name="it"] > MemberExpression > Identifier[name="deflakeOnly"]', + }, + ], + }, + }, + ], +}; diff --git a/remote/test/puppeteer/test/README.md b/remote/test/puppeteer/test/README.md new file mode 100644 index 0000000000..72085ecfb2 --- /dev/null +++ b/remote/test/puppeteer/test/README.md @@ -0,0 +1,95 @@ +# Puppeteer tests + +Unit tests in Puppeteer are written using [Mocha] as the test runner and [Expect] as the assertions library. + +## Test state + +We have some common setup that runs before each test and is defined in `mocha-utils.js`. + +You can use the `getTestState` function to read state. It exposes the following that you can use in your tests. These will be reset/tidied between tests automatically for you: + +- `puppeteer`: an instance of the Puppeteer library. This is exactly what you'd get if you ran `require('puppeteer')`. +- `puppeteerPath`: the path to the root source file for Puppeteer. +- `defaultBrowserOptions`: the default options the Puppeteer browser is launched from in test mode, so tests can use them and override if required. +- `server`: a dummy test server instance (see `packages/testserver` for more). +- `httpsServer`: a dummy test server HTTPS instance (see `packages/testserver` for more). +- `isFirefox`: true if running in Firefox. +- `isChrome`: true if running Chromium. +- `isHeadless`: true if the test is in headless mode. + +If your test needs a browser instance, you can use the `setupTestBrowserHooks()` function which will automatically configure a browser that will be cleaned between each test suite run. You access this via `getTestState()`. + +If your test needs a Puppeteer page and context, you can use the `setupTestPageAndContextHooks()` function which will configure these. You can access `page` and `context` from `getTestState()` once you have done this. + +The best place to look is an existing test to see how they use the helpers. + +## Skipping tests in specific conditions + +To skip tests edit the [TestExpectations](https://github.com/puppeteer/puppeteer/blob/main/test/TestExpectations.json) file. See [test runner documentation](https://github.com/puppeteer/puppeteer/tree/main/tools/mocha-runner) for more details. + +## Running tests + +- To run all tests applicable for your platform: + +```bash +npm test +``` + +- **Important**: don't forget to first build the code if you're testing local changes: + +```bash +npm run build --workspace=@puppeteer-test/test && npm test +``` + +### CLI options + +| Description | Option | Type | +| ----------------------------------------------------------------- | ---------------- | ------- | +| Do not generate coverage report | --no-coverage | boolean | +| Do not generate suggestion for updating TestExpectation.json file | --no-suggestions | boolean | +| Specify a file to which to save run data | --save-stats-to | string | +| Specify a file with a custom Mocha reporter | --reporter | string | +| Number of times to retry failed tests. | --retries | number | +| Timeout threshold value. | --timeout | number | +| Tell Mocha to not run test files in parallel | --no-parallel | boolean | +| Generate full stacktrace upon failure | --fullTrace | boolean | +| Name of the Test suit defined in TestSuites.json | --test-suite | string | + +### Helpful information + +- To run a specific test, substitute the `it` with `it.only`: + +```ts + ... + it.only('should work', async function() { + const {server, page} = await getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To disable a specific test, substitute the `it` with `it.skip`: + +```ts + ... + it.skip('should work', async function({server, page}) { + const {server, page} = await getTestState(); + const response = await page.goto(server.EMPTY_PAGE); + expect(response.ok).toBe(true); + }); +``` + +- To run Chrome headful tests: + +```bash +npm run test:chrome:headful +``` + +- To run tests with custom browser executable: + +```bash +BINARY=<path-to-executable> npm run test:chrome:headless # Or npm run test:firefox +``` + +[mocha]: https://mochajs.org/ +[expect]: https://www.npmjs.com/package/expect diff --git a/remote/test/puppeteer/test/TestExpectations.json b/remote/test/puppeteer/test/TestExpectations.json new file mode 100644 index 0000000000..f19c2f72e0 --- /dev/null +++ b/remote/test/puppeteer/test/TestExpectations.json @@ -0,0 +1,3714 @@ +[ + { + "testIdPattern": "*", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP", "TIMEOUT"] + }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[bfcache.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[click.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[debugInfo.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[debugInfo.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[device-request-prompt.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[dialog.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[drag-and-drop.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[drag-and-drop.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[drag-and-drop.spec] Legacy Drag n' Drop *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[elementhandle.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[fixtures.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[injected.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[jshandle.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch *", + "platforms": ["darwin", "linux"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch *", + "platforms": ["win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[locator.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[mouse.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goBack *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate *", + "platforms": ["darwin", "linux"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate *", + "platforms": ["win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[network.spec] network Page.setExtraHTTPHeaders *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[network.spec] network Response.json *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.text *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[prerender.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryselector.spec] querySelector *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screencast.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Cdp *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[stacktrace.spec] Stack trace *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[accessibility.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP", "TIMEOUT"] + }, + { + "testIdPattern": "[accessibility.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne (Chromium web test) *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should have an error message specifically for awaiting an element to be hidden", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should have correct stack trace for timeout", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should respect timeout", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler waitForSelector (aria) should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[autofill.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.process should return child_process instance", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.target should return browser target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should be prompt by default", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should close all belonging targets once closing context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should create new incognito context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should have default context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should timeout waiting for a non-existent target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext window.open should use parent tab context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should not report created targets for custom CDP sessions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[chromiumonly.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |pipe| option *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[Connection.spec] WebDriver BiDi Connection should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[coverage.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[coverage.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should ignore pptr internal scripts if reportAnonymousScripts is true", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools should open devtools when \"devtools: true\" option is given", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if asPage is used", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if custom isPageTarget is provided", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[drag-and-drop.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should not work if the click box is not visible due to the iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[emulation.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should get the proper viewport size", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should support mobile emulation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should replace symbols with undefined", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should return properly serialize objects with unknown type fields", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work for circular object", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluateOnNewDocument *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.removeScriptToEvaluateOnNewDocument *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.removeScriptToEvaluateOnNewDocument *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[fixtures.spec] Fixtures dumpio option should work with pipe option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should handle nested frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.name()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[headful.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[idle_override.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[input.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should not throw for circular objects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle JSHandle.jsonValue should work with dates", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should return the RemoteObject", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[jshandle.spec] JSHandle Page.evaluateHandle should use the same JS wrappers", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["TIMEOUT"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect *", + "platforms": ["win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject waitForSelector when browser closes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath returns executablePath for channel", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath when executable path is configured its value is used", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Chrome", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should have custom URL when launching browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should launch Chrome properly with --no-startup-window and waitForInitialPage=false", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", + "platforms": ["linux"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should work with no default arguments", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir argument with non-existent dir", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should not throw if buttons are pressed twice", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should reset properly", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when navigating to bad SSL", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should fire events in proper order", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Page.setBypassServiceWorker *", + "platforms": ["win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Cross-origin set-cookie", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.initiator should return the initiator", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work when navigating to image", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.postData should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.postData should work with blobs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "Not implemented for BiDi yet." + }, + { + "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[network.spec] network Response.timing returns timing information", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[oopif.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should provide access to elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support evaluating in oop iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support frames within OOP frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support frames within OOP iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should track navigations within OOP iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should treat OOP iframes and normal iframes the same", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF waitForFrame should resolve immediately if the frame already exists", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.addStyleTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.bringToFront should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should *not* run beforeunload by default", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should *not* run beforeunload by default", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location when fetch fails", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should trigger correct Log", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.error should throw when page crashes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should work with loading frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"], + "comment": "Missing request interception" + }, + { + "testIdPattern": "[page.spec] Page Page.metrics metrics event fired on console.timeStamp", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.metrics should get metrics from a page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.pdf can print to PDF with accessible", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.pdf should respect timeout", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.removeExposedFunction should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should stay disabled when toggling request interception on/off", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setGeolocation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should emulate navigator.onLine", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setUserAgent *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.waitForNetworkIdle should work with aborted requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[prerender.spec] Prerender can screencast", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[prerender.spec] Prerender can screencast", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[proxy.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at browser level", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at context level", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with name and role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work with :hover", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests Text selectors in Page should clear caches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryObjects.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[requestinterception.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[screencast.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screencast.spec] Screencasts Page.screencast should validate options", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should get screenshot bigger than the viewport", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[stacktrace.spec] Stack trace *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[target.spec] Target Browser.pages should return all of the pages", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target Browser.targets should return all of the targets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target Browser.waitForTarget should timeout waiting for a non-existent target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[target.spec] Target should be able to use async waitForTarget", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should be able to use the default page in the browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should contain browser target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should report when a new page is created and closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should report when a target url changes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[TargetManager.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "SKIP"] + }, + { + "testIdPattern": "[touchscreen.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[tracing.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[tracing.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[worker.spec] *", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[accessibility.spec] Accessibility filtering children of leaf nodes rich text editable fields should have children", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[accessibility.spec] Accessibility get snapshots while the tree is re-calculated", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler parseAriaSelector should find button", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryAll should find menu by name", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryAllArray $$eval should handle many elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find button by name and role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find button by role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find by name", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ariaqueryhandler.spec] AriaQueryHandler queryOne should find first matching element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[bfcache.spec] BFCache can navigate to a BFCached page containing an OOPIF and a worker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.isConnected should set the browser connected state", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.process should not return child_process for remote browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.process should not return child_process for remote browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.target should return browser target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browser.spec] Browser specs Browser.version should return version", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should deny permission when not listed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant permission when listed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should grant persistent-storage", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should isolate permissions between browser contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should reset permissions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext BrowserContext.overridePermissions should trigger permission onchange", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should close all belonging targets once closing context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should create new incognito context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should fire target events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should provide a context id", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should timeout waiting for a non-existent target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext should work across sessions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[browsercontext.spec] BrowserContext window.open should use parent tab context", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should be able to detach session", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should enable and disable domains independently", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should respect custom timeout", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should throw nice errors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |browserURL| option should be able to connect using browserUrl, with and without trailing slash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[click.spec] Page.click should click on checkbox input and toggle", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click on checkbox label and toggle", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button if window.Node is removed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with deviceScaleFactor set", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[click.spec] Page.click should click the button with fixed position inside an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[click.spec] Page.click should click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should double click the button", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should scroll and click with disabled javascript", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[click.spec] Page.click should select the text by triple clicking", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[click.spec] Page.click should select the text by triple clicking", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.cookies should get cookies from multiple urls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.deleteCookie should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should default to setting secure cookie for HTTPS websites", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should isolate cookies in browser contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie on a different domain", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set a cookie with a path", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookie with reasonable defaults", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set cookies from a frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set multiple cookies", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should set secure same-site cookies from a frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[cookies.spec] Cookie specs Page.setCookie should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[coverage.spec] Coverage specs CSSCoverage should work with complicated usecases", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[coverage.spec] Coverage specs JSCoverage should not ignore eval() scripts if reportAnonymousScripts is true", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.cookies() should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.deleteCookie() should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[defaultbrowsercontext.spec] DefaultBrowserContext page.setCookie() should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[dialog.spec] Page.Events.Dialog should allow accepting prompts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[drag-and-drop.spec] Drag n' Drop should drag and drop", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boundingBox should handle nested frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.boxModel should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.click should return Point data", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.clickablePoint should work for iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.contentFrame should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isIntersectingViewport should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[elementhandle.spec] ElementHandle specs ElementHandle.isIntersectingViewport should work with svg elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulate should support clicking", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateCPUThrottling should change the CPU throttling rate successfully", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should throw in case of bad argument", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaFeatures should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaType should throw in case of bad argument", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateMediaType should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateNetworkConditions should change navigator.connection.effectiveType", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateNetworkConditions should change navigator.connection.effectiveType", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should throw for invalid timezone IDs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateTimezone should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateVisionDeficiency should throw for invalid vision deficiencies", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.emulateVisionDeficiency should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should detect touch when applying viewport with touches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should get the proper viewport size", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should support landscape emulation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[emulation.spec] Emulation Page.viewport should support touch emulation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Frame.evaluate should have different execution contexts", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should await promise", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should simulate a user gesture", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw if elementHandles are from other frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should throw when evaluation triggers reload", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[evaluation.spec] Evaluation specs Page.evaluate should work from-inside an exposed function", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions background_page target type should be available", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions service_worker target type should be available", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[fixtures.spec] Fixtures should close the browser when the node process closes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should detach child frames on navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report different frame instance when frame re-attaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame from-inside shadow DOM", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.name()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.parent()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should report frame.parent()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should send events when frames are manipulated dynamically", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should support framesets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame Management should support lazy frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame.evaluate should throw for detached frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame.evaluate should throw for detached frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[frame.spec] Frame specs Frame.executionContext should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails Network redirects should report SecurityDetails", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors Response.securityDetails should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with mixed content", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[ignorehttpserrors.spec] ignoreHTTPSErrors should work with request interception", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard ElementHandle.press should not support |text| option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should report shiftKey", + "platforms": ["darwin"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should send a character with sendCharacter in iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should specify location", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should specify repeat property", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type all kinds of characters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type emoji", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type emoji into an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[keyboard.spec] Keyboard should type emoji into an iframe", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Browser target events should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Browser.Events.disconnected should be emitted when: browser gets closed, disconnected or underlying websocket gets closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.close should terminate network waiters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.close should terminate network waiters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Browser.disconnect should reject navigation when browser closes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to close remote browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect multiple times to the same browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to a browser with no page targets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to a browser with no page targets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to the same page simultaneously", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to connect to the same page simultaneously", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to reconnect", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should be able to reconnect to a disconnected browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support ignoreHTTPSErrors option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support targetFilter option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.connect should support targetFilter option", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath returns executablePath for channel", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.executablePath should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch can launch and close the browser", + "platforms": ["win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Chrome", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should be able to launch Firefox", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should close browser with beforeunload page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default argument in Firefox", + "platforms": ["linux"], + "parameters": ["firefox", "headful"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should filter out ignored default arguments in Chrome", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch should have custom URL when launching browser", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch tmp profile should be cleaned up", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir option should restore cookies", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[locator.spec] Locator Locator.click should work with a OOPIF", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[locator.spec] Locator Locator.race races multiple locators", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should send mouse wheel events", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[mouse.spec] Mouse should trigger hover state with removed window.Node", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should navigate subframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should reject when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.goto should return matching responses", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should fail when frame detaches", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Frame.waitForNavigation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goBack should work with HistoryAPI", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when main resources failed to load", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should fail when server returns 204", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to dataURL and fire dataURL requests", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle0", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to empty page with networkidle2", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to page with iframe and networkidle0", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should not leak listeners during navigation of 11 pages", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should return response when page changes its URL after load", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should send referer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should wait for network idle to succeed navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when navigating to data url", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work when page calls history API in beforeunload", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", + "platforms": ["linux"], + "parameters": ["chrome", "headless"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with anchor navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "headless"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.goto should work with subframes return 204", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work when subframe issues window.stop()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with DOM history.back()/history.forward()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.pushState()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[navigation.spec] navigation Page.waitForNavigation should work with history.replaceState()", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFailed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestFinished", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.RequestServedFromCache", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Response", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should fire events in proper order", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should fire events in proper order", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Network Events should support redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Network Events should support redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should allow disable authentication", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should fail if wrong credentials", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should not disable caching", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Page.authenticate should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Page.Events.Request should fire for iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Page.Events.Request should fire for iframes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Page.setExtraHTTPHeaders should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.frame should work for subframe navigation request", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Request.initiator should return the initiator", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.isNavigationRequest should work with request interception", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.postData should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Request.postData should work with blobs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"], + "comment": "Blobs have no POST data in Firefox's CDP implementation." + }, + { + "testIdPattern": "[network.spec] network Response.buffer should throw if the response does not have a body", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.buffer should work with compression", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.fromCache should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.fromCache should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network Response.fromCache should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", + "platforms": ["win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.fromServiceWorker Response.fromServiceWorker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.headers should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[network.spec] network Response.json should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should return uncompressed text", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should throw when requesting body of redirected response", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[network.spec] network Response.text should wait until response completes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should wait until response completes", + "platforms": ["win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.text should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Response.timing returns timing information", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should keep track of a frames OOP state", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should provide access to elements", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support lazy OOP frames", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[oopif.spec] OOPIF should support wait for navigation for transitions from local to OOPIF", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.addScriptTag should throw when added with content to the CSP page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.bringToFront should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.client should return the client instance", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should reject all promises when page is closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should run beforeunload if asked for", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.close should terminate network waiters", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location and stack trace for console API calls", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should have location when fetch fails", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should not fail for window object", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should trigger correct Log", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with logging functions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Console should work for different console API calls with timing functions", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.error should throw when page crashes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and rel=noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and with rel=opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with clicking target=_blank and without rel=opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with fake-clicking target=_blank and rel=noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.Events.Popup should work with noopener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.exposeFunction should be callable from-inside evaluateOnNewDocument", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.metrics metrics event fired on console.timeStamp", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.metrics should get metrics from a page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.pdf can print to PDF with accessible", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[page.spec] Page Page.removeExposedFunction should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.select should work when re-defining top-level Event class", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass after cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass after cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP header", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP header", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP in iframes as well", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP in iframes as well", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP meta tag", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setBypassCSP should bypass CSP meta tag", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should enable or disable the cache based on the state passed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should enable or disable the cache based on the state passed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setCacheEnabled should stay disabled when toggling request interception on/off", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setGeolocation should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.setJavaScriptEnabled should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should emulate navigator.onLine", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setOfflineMode should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[page.spec] Page Page.setUserAgent should work with additional userAgentMetdata", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[prerender.spec] Prerender can navigate to a prerendered page via Puppeteer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[prerender.spec] Prerender can screencast", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[prerender.spec] Prerender via frame can navigate to a prerendered page via Puppeteer", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at browser level", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should proxy requests when configured at context level", + "platforms": ["linux"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at browser level", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy in incognito browser context should respect proxy bypass list when configured at context level", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy should proxy requests when configured", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[proxy.spec] request proxy should respect proxy bypass list", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with name and role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work ARIA selectors with role", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryhandler.spec] Query handler tests P selectors should work for ARIA selectors in multiple isolated worlds", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "TIMEOUT"] + }, + { + "testIdPattern": "[queryObjects.spec] page.queryObjects should fail for disposed handles", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryObjects.spec] page.queryObjects should fail primitive values as prototypes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[queryObjects.spec] page.queryObjects should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[queryObjects.spec] page.queryObjects should work for non-trivial page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception \"after each\" hook in \"request interception\"", + "platforms": ["win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should load fonts if cache enabled", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception-experimental.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should navigate to URL with hash and fire requests without hash", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Cdp should use scale for clip", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should capture full element when larger than viewport", + "platforms": ["win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should clip clip bigger than the viewport without \"captureBeyondViewport\"", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[stacktrace.spec] Stack trace should work for none error objects", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target Browser.targets should return all of the targets", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should be able to use async waitForTarget", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should contain browser target", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[target.spec] Target should create a worker from a service worker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should create a worker from a shared worker", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should have an opener", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should not crash while redirecting if original request was missed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[target.spec] Target should not report uninitialized pages", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[target.spec] Target should report when a new page is created and closed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a service worker is created and destroyed", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a target url changes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[target.spec] Target should report when a target url changes", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[touchscreen.spec] Touchscreen Touchscreen.prototype.touchMove should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should survive navigations", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work when resolved right before execution context disposal", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work with strict CSP policy", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForFunction should work with strict CSP policy", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector Page.waitForSelector is shortcut for main frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector Page.waitForSelector is shortcut for main frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["TIMEOUT"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["FAIL", "PASS", "TIMEOUT"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should survive cross-process navigation", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForSelector should work with removed MutationObserver", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should run in specified frame", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[waittask.spec] waittask specs Frame.waitForXPath should throw when frame is detached", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[worker.spec] Workers should report console logs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[worker.spec] Workers should report console logs", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[worker.spec] Workers should report errors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["firefox", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[worker.spec] Workers should report errors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[worker.spec] Workers should report errors", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["chrome", "webDriverBiDi"], + "expectations": ["PASS"] + }, + { + "testIdPattern": "[CDPSession.spec] Target.createCDPSession should send events", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[chromiumonly.spec] Chromium-Specific Launcher tests Puppeteer.launch |pipe| option should fire \"disconnected\" when closing with pipe", + "platforms": ["darwin"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL"], + "comment": "Remove with M121" + }, + { + "testIdPattern": "[devtools.spec] DevTools should expose DevTools as a page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools should open devtools when \"devtools: true\" option is given", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if asPage is used", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[devtools.spec] DevTools target.page() should return a DevTools page if custom isPageTarget is provided", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions background_page target type should be available", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions service_worker target type should be available", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[extensions.spec] extensions target.page() should return a background_page", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[launcher.spec] Launcher specs Puppeteer Puppeteer.launch userDataDir option restores preferences", + "platforms": ["win32"], + "parameters": ["firefox", "headless", "webDriverBiDi"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[network.spec] network Network Events Page.Events.Request", + "platforms": ["linux"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[network.spec] network raw network headers Same-origin set-cookie subresource", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[page.spec] Page Page.bringToFront should work", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should be abortable", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "new-headless"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[requestinterception.spec] request interception Page.setRequestInterception should work with redirects", + "platforms": ["win32"], + "parameters": ["cdp", "chrome", "headful"], + "expectations": ["FAIL", "PASS"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Cdp should work in \"fromSurface: false\" mode", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "headless"], + "expectations": ["SKIP"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headful"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work for an element with an offset", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headful"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots ElementHandle.screenshot should work with a rotated element", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headful"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[screenshot.spec] Screenshots Page.screenshot should take fullPage screenshots", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "firefox", "headless"], + "expectations": ["FAIL"] + }, + { + "testIdPattern": "[worker.spec] Workers Page.workers", + "platforms": ["darwin", "linux", "win32"], + "parameters": ["cdp", "chrome", "headless"], + "expectations": ["FAIL", "PASS"] + } +] diff --git a/remote/test/puppeteer/test/TestSuites.json b/remote/test/puppeteer/test/TestSuites.json new file mode 100644 index 0000000000..32adc45d3c --- /dev/null +++ b/remote/test/puppeteer/test/TestSuites.json @@ -0,0 +1,74 @@ +{ + "testSuites": [ + { + "id": "chrome-headless", + "platforms": ["linux", "win32", "darwin"], + "parameters": ["chrome", "headless", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "chrome-headful", + "platforms": ["linux"], + "parameters": ["chrome", "headful", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "chrome-new-headless", + "platforms": ["linux"], + "parameters": ["chrome", "new-headless", "cdp"], + "expectedLineCoverage": 93 + }, + { + "id": "firefox-headless", + "platforms": ["linux", "darwin"], + "parameters": ["firefox", "headless", "cdp"], + "expectedLineCoverage": 80 + }, + { + "id": "firefox-headful", + "platforms": ["linux"], + "parameters": ["firefox", "headful", "cdp"], + "expectedLineCoverage": 80 + }, + { + "id": "firefox-bidi", + "platforms": ["linux"], + "parameters": ["firefox", "headless", "webDriverBiDi"], + "expectedLineCoverage": 56 + }, + { + "id": "firefox-bidi-headful", + "platforms": ["linux"], + "parameters": ["firefox", "headful", "webDriverBiDi"], + "expectedLineCoverage": 56 + }, + { + "id": "chrome-bidi", + "platforms": ["linux"], + "parameters": ["chrome", "headless", "webDriverBiDi"], + "expectedLineCoverage": 56 + } + ], + "parameterDefinitions": { + "chrome": { + "PUPPETEER_PRODUCT": "chrome" + }, + "firefox": { + "PUPPETEER_PRODUCT": "firefox" + }, + "headless": { + "HEADLESS": "true", + "PUPPETEER_LOGLEVEL": "silent" + }, + "headful": { + "HEADLESS": "false" + }, + "new-headless": { + "HEADLESS": "new" + }, + "webDriverBiDi": { + "PUPPETEER_PROTOCOL": "webDriverBiDi" + }, + "cdp": {} + } +} diff --git a/remote/test/puppeteer/test/assets/abort-request.html b/remote/test/puppeteer/test/assets/abort-request.html new file mode 100644 index 0000000000..77c056a422 --- /dev/null +++ b/remote/test/puppeteer/test/assets/abort-request.html @@ -0,0 +1,13 @@ +<button id="abort"></button> + +<script> + const button = document.getElementById('abort'); + button.addEventListener('click', getJson) + async function getJson() { + const abort = new AbortController(); + const result = fetch("/simple.json", { + signal: abort.signal + }); + abort.abort(); + } +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/beforeunload.html b/remote/test/puppeteer/test/assets/beforeunload.html new file mode 100644 index 0000000000..3cef6763f3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/beforeunload.html @@ -0,0 +1,10 @@ +<div>beforeunload demo.</div> +<script> +window.addEventListener('beforeunload', event => { + // Chrome way. + event.returnValue = 'Leave?'; + // Firefox way. + event.preventDefault(); +}); +</script> + diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/index.html b/remote/test/puppeteer/test/assets/cached/bfcache/index.html new file mode 100644 index 0000000000..3d79312828 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/bfcache/index.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body>BFCached<a href="target.html">next</a></body> diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/target.html b/remote/test/puppeteer/test/assets/cached/bfcache/target.html new file mode 100644 index 0000000000..eafc537b64 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/bfcache/target.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body>target</body> diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html new file mode 100644 index 0000000000..857914bb6d --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe-container.html @@ -0,0 +1,11 @@ +<body>BFCached<a href="target.html">next</a></body> +<script> + window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; + url.pathname = '/cached/bfcache/worker-iframe.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); + }, false); +</script> diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html new file mode 100644 index 0000000000..9233f557c5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker-iframe.html @@ -0,0 +1,3 @@ +<script> + const worker = new Worker('worker.mjs', {type: 'module'}) +</script> diff --git a/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs b/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs new file mode 100644 index 0000000000..72a8036e68 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/bfcache/worker.mjs @@ -0,0 +1 @@ +console.log('HELLO'); diff --git a/remote/test/puppeteer/test/assets/cached/one-style-font.css b/remote/test/puppeteer/test/assets/cached/one-style-font.css new file mode 100644 index 0000000000..6178de0350 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style-font.css @@ -0,0 +1,9 @@ +@font-face { + font-family: 'one-style'; + src: url('./one-style.woff') format('woff'); +} + +body { + background-color: pink; + font-family: 'one-style', sans-serif; +} diff --git a/remote/test/puppeteer/test/assets/cached/one-style-font.html b/remote/test/puppeteer/test/assets/cached/one-style-font.html new file mode 100644 index 0000000000..8e7236dfb3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style-font.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style-font.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/cached/one-style.css b/remote/test/puppeteer/test/assets/cached/one-style.css new file mode 100644 index 0000000000..04e7110b41 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/cached/one-style.html b/remote/test/puppeteer/test/assets/cached/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/cached/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/consolelog.html b/remote/test/puppeteer/test/assets/consolelog.html new file mode 100644 index 0000000000..4a27803aa9 --- /dev/null +++ b/remote/test/puppeteer/test/assets/consolelog.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <title>console.log test</title> + </head> + <body> + <script> + function foo() { + console.log('yellow') + } + function bar() { + foo(); + } + bar(); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/credit-card.html b/remote/test/puppeteer/test/assets/credit-card.html new file mode 100644 index 0000000000..101013a0ca --- /dev/null +++ b/remote/test/puppeteer/test/assets/credit-card.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> +</head> + +<body> + <form id="testform" method="post"> + <table> + <tbody> + <tr> + <td> + <label for="name">Name on Card</label> + </td> + <td> + <input size="40" id="name" /> + </td> + </tr> + <tr> + <td> + <label for="number">Card Number</label> + </td> + <td> + <input size="40" id="number" name="card_number" /> + </td> + </tr> + <tr> + <td> + <label>Expiration Date</label> + </td> + <td> + <input size="2" id="expiration_month" name="ccmonth"> <input size="4" id="expiration_year" + name="ccyear" /> + </td> + </tr> + </tbody> + </table> + <input type="submit" value="Submit"> + </form> +</body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/csp.html b/remote/test/puppeteer/test/assets/csp.html new file mode 100644 index 0000000000..34fc1fc1a5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csp.html @@ -0,0 +1 @@ +<meta http-equiv="Content-Security-Policy" content="default-src 'self'"> diff --git a/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf Binary files differnew file mode 100644 index 0000000000..4b208624e8 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/Dosis-Regular.ttf diff --git a/remote/test/puppeteer/test/assets/csscoverage/OFL.txt b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt new file mode 100644 index 0000000000..a9b3c8b34e --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/OFL.txt @@ -0,0 +1,95 @@ +Copyright (c) 2011, Edgar Tolentino and Pablo Impallari (www.impallari.com|impallari@gmail.com), +Copyright (c) 2011, Igino Marini. (www.ikern.com|mail@iginomarini.com), +with Reserved Font Names "Dosis". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/remote/test/puppeteer/test/assets/csscoverage/empty.html b/remote/test/puppeteer/test/assets/csscoverage/empty.html new file mode 100644 index 0000000000..b3845c366d --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/empty.html @@ -0,0 +1,3 @@ +<style></style> +<div>empty style tag</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/involved.html b/remote/test/puppeteer/test/assets/csscoverage/involved.html new file mode 100644 index 0000000000..bcd9845b93 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/involved.html @@ -0,0 +1,26 @@ +<style> +@charset "utf-8"; +@namespace svg url(http://www.w3.org/2000/svg); +@font-face { + font-family: "Example Font"; + src: url("./Dosis-Regular.ttf"); +} + +#fluffy { + border: 1px solid black; + z-index: 1; + /* -webkit-disabled-property: rgb(1, 2, 3) */ + -lol-cats: "dogs" /* non-existing property */ +} + +@media (min-width: 1px) { + span { + -webkit-border-radius: 10px; + font-family: "Example Font"; + animation: 1s identifier; + } +} +</style> +<div id="fluffy">woof!</div> +<span>fancy text</span> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/media.html b/remote/test/puppeteer/test/assets/csscoverage/media.html new file mode 100644 index 0000000000..bfb89f8f75 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/media.html @@ -0,0 +1,4 @@ +<style> +@media screen { div { color: green; } } </style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/multiple.html b/remote/test/puppeteer/test/assets/csscoverage/multiple.html new file mode 100644 index 0000000000..0fd97e962a --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/multiple.html @@ -0,0 +1,8 @@ +<link rel="stylesheet" href="stylesheet1.css"> +<link rel="stylesheet" href="stylesheet2.css"> +<script> +window.addEventListener('DOMContentLoaded', () => { + // Force stylesheets to load. + console.log(window.getComputedStyle(document.body).color); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/csscoverage/simple.html b/remote/test/puppeteer/test/assets/csscoverage/simple.html new file mode 100644 index 0000000000..3beae21829 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/simple.html @@ -0,0 +1,6 @@ +<style> +div { color: green; } +a { color: blue; } +</style> +<div>hello, world</div> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html new file mode 100644 index 0000000000..df4e9c276c --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/sourceurl.html @@ -0,0 +1,7 @@ +<style> +body { + padding: 10px; +} +/*# sourceURL=nicename.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css new file mode 100644 index 0000000000..60f1eab971 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet1.css @@ -0,0 +1,3 @@ +body { + color: red; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css new file mode 100644 index 0000000000..a87defb098 --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/stylesheet2.css @@ -0,0 +1,4 @@ +html { + margin: 0; + padding: 0; +} diff --git a/remote/test/puppeteer/test/assets/csscoverage/unused.html b/remote/test/puppeteer/test/assets/csscoverage/unused.html new file mode 100644 index 0000000000..5b8186a3bf --- /dev/null +++ b/remote/test/puppeteer/test/assets/csscoverage/unused.html @@ -0,0 +1,7 @@ +<style> +@media screen { + a { color: green; } +} +/*# sourceURL=unused.css */ +</style> + diff --git a/remote/test/puppeteer/test/assets/detect-touch.html b/remote/test/puppeteer/test/assets/detect-touch.html new file mode 100644 index 0000000000..80a4123fbd --- /dev/null +++ b/remote/test/puppeteer/test/assets/detect-touch.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <title>Detect Touch Test</title> + <script src='modernizr.js'></script> + </head> + <body style="font-size:30vmin"> + <script> + document.body.textContent = Modernizr.touchevents ? 'YES' : 'NO'; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/digits/0.png b/remote/test/puppeteer/test/assets/digits/0.png Binary files differnew file mode 100644 index 0000000000..ac3c4768ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/0.png diff --git a/remote/test/puppeteer/test/assets/digits/1.png b/remote/test/puppeteer/test/assets/digits/1.png Binary files differnew file mode 100644 index 0000000000..6768222729 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/1.png diff --git a/remote/test/puppeteer/test/assets/digits/2.png b/remote/test/puppeteer/test/assets/digits/2.png Binary files differnew file mode 100644 index 0000000000..b1daa4735d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/2.png diff --git a/remote/test/puppeteer/test/assets/digits/3.png b/remote/test/puppeteer/test/assets/digits/3.png Binary files differnew file mode 100644 index 0000000000..6eca99b21b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/3.png diff --git a/remote/test/puppeteer/test/assets/digits/4.png b/remote/test/puppeteer/test/assets/digits/4.png Binary files differnew file mode 100644 index 0000000000..a721071e2c --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/4.png diff --git a/remote/test/puppeteer/test/assets/digits/5.png b/remote/test/puppeteer/test/assets/digits/5.png Binary files differnew file mode 100644 index 0000000000..15cb19932a --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/5.png diff --git a/remote/test/puppeteer/test/assets/digits/6.png b/remote/test/puppeteer/test/assets/digits/6.png Binary files differnew file mode 100644 index 0000000000..639f38439d --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/6.png diff --git a/remote/test/puppeteer/test/assets/digits/7.png b/remote/test/puppeteer/test/assets/digits/7.png Binary files differnew file mode 100644 index 0000000000..5c1150b005 --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/7.png diff --git a/remote/test/puppeteer/test/assets/digits/8.png b/remote/test/puppeteer/test/assets/digits/8.png Binary files differnew file mode 100644 index 0000000000..abb8b48b0b --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/8.png diff --git a/remote/test/puppeteer/test/assets/digits/9.png b/remote/test/puppeteer/test/assets/digits/9.png Binary files differnew file mode 100644 index 0000000000..6a40a21c6f --- /dev/null +++ b/remote/test/puppeteer/test/assets/digits/9.png diff --git a/remote/test/puppeteer/test/assets/dynamic-oopif.html b/remote/test/puppeteer/test/assets/dynamic-oopif.html new file mode 100644 index 0000000000..38614d0289 --- /dev/null +++ b/remote/test/puppeteer/test/assets/dynamic-oopif.html @@ -0,0 +1,10 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = url.hostname === 'localhost' ? '127.0.0.1' : 'localhost'; + url.pathname = '/oopif.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); +}, false); +</script> diff --git a/remote/test/puppeteer/test/assets/empty.html b/remote/test/puppeteer/test/assets/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/empty.html diff --git a/remote/test/puppeteer/test/assets/error.html b/remote/test/puppeteer/test/assets/error.html new file mode 100644 index 0000000000..130400c006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/error.html @@ -0,0 +1,15 @@ +<script> +a(); + +function a() { + b(); +} + +function b() { + c(); +} + +function c() { + throw new Error('Fancy error!'); +} +</script> diff --git a/remote/test/puppeteer/test/assets/es6/.eslintrc b/remote/test/puppeteer/test/assets/es6/.eslintrc new file mode 100644 index 0000000000..1903e176f5 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/.eslintrc @@ -0,0 +1,5 @@ +{ + "parserOptions": { + "sourceType": "module" + } +}
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/es6/es6import.js b/remote/test/puppeteer/test/assets/es6/es6import.js new file mode 100644 index 0000000000..9aac2d4d64 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6import.js @@ -0,0 +1,2 @@ +import num from './es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/es6/es6module.js b/remote/test/puppeteer/test/assets/es6/es6module.js new file mode 100644 index 0000000000..7a4e8a723a --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6module.js @@ -0,0 +1 @@ +export default 42; diff --git a/remote/test/puppeteer/test/assets/es6/es6pathimport.js b/remote/test/puppeteer/test/assets/es6/es6pathimport.js new file mode 100644 index 0000000000..eb17a9a3d1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/es6/es6pathimport.js @@ -0,0 +1,2 @@ +import num from './es6/es6module.js'; +window.__es6injected = num; diff --git a/remote/test/puppeteer/test/assets/favicon.ico b/remote/test/puppeteer/test/assets/favicon.ico Binary files differnew file mode 100644 index 0000000000..d4edd50799 --- /dev/null +++ b/remote/test/puppeteer/test/assets/favicon.ico diff --git a/remote/test/puppeteer/test/assets/file-to-upload.txt b/remote/test/puppeteer/test/assets/file-to-upload.txt new file mode 100644 index 0000000000..b4ad118489 --- /dev/null +++ b/remote/test/puppeteer/test/assets/file-to-upload.txt @@ -0,0 +1 @@ +contents of the file
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/frames/frame.html b/remote/test/puppeteer/test/assets/frames/frame.html new file mode 100644 index 0000000000..8f20d2da9f --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frame.html @@ -0,0 +1,8 @@ +<link rel='stylesheet' href='./style.css'> +<script src='./script.js' type='text/javascript'></script> +<style> +div { + line-height: 18px; +} +</style> +<div>Hi, I'm frame</div> diff --git a/remote/test/puppeteer/test/assets/frames/frameset.html b/remote/test/puppeteer/test/assets/frames/frameset.html new file mode 100644 index 0000000000..4d56f88839 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/frameset.html @@ -0,0 +1,8 @@ +<frameset> + <frameset> + <frame src='./frame.html'></frame> + <frame src='about:blank'></frame> + </frameset> + <frame src='/empty.html'></frame> + <frame></frame> +</frameset> diff --git a/remote/test/puppeteer/test/assets/frames/lazy-frame.html b/remote/test/puppeteer/test/assets/frames/lazy-frame.html new file mode 100644 index 0000000000..4821cd76cd --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/lazy-frame.html @@ -0,0 +1,3 @@ +<iframe width="100%" height="300" src="about:blank"></iframe> +<div style="height: 800vh"></div> +<iframe width="100%" height="300" src='./frame.html' loading="lazy"></iframe>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/frames/nested-frames.html b/remote/test/puppeteer/test/assets/frames/nested-frames.html new file mode 100644 index 0000000000..e9c5d83c03 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/nested-frames.html @@ -0,0 +1,26 @@ +<style> +:root { + scrollbar-width: none; +} + +body { + display: flex; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +</style> +<script> +async function attachFrame(frameId, url) { + var frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => frame.onload = x); + return 'kazakh'; +} +</script> +<iframe src='./two-frames.html' name='2frames'></iframe> +<iframe src='./frame.html' name='aframe'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html new file mode 100644 index 0000000000..d1462641ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame-url-fragment.html @@ -0,0 +1 @@ +<iframe src='./frame.html?param=value#fragment'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/one-frame.html b/remote/test/puppeteer/test/assets/frames/one-frame.html new file mode 100644 index 0000000000..e941d795a2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/one-frame.html @@ -0,0 +1 @@ +<iframe src='./frame.html'></iframe> diff --git a/remote/test/puppeteer/test/assets/frames/script.js b/remote/test/puppeteer/test/assets/frames/script.js new file mode 100644 index 0000000000..be22256d16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/script.js @@ -0,0 +1 @@ +console.log('Cheers!'); diff --git a/remote/test/puppeteer/test/assets/frames/style.css b/remote/test/puppeteer/test/assets/frames/style.css new file mode 100644 index 0000000000..5b5436e874 --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/style.css @@ -0,0 +1,3 @@ +div { + color: blue; +} diff --git a/remote/test/puppeteer/test/assets/frames/two-frames.html b/remote/test/puppeteer/test/assets/frames/two-frames.html new file mode 100644 index 0000000000..b2ee853eda --- /dev/null +++ b/remote/test/puppeteer/test/assets/frames/two-frames.html @@ -0,0 +1,13 @@ +<style> +body { + display: flex; + flex-direction: column; +} + +body iframe { + flex-grow: 1; + flex-shrink: 1; +} +</style> +<iframe src='./frame.html' name='uno'></iframe> +<iframe src='./frame.html' name='dos'></iframe> diff --git a/remote/test/puppeteer/test/assets/global-var.html b/remote/test/puppeteer/test/assets/global-var.html new file mode 100644 index 0000000000..b6be975038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/global-var.html @@ -0,0 +1,3 @@ +<script> +var globalVar = 123; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/grid.html b/remote/test/puppeteer/test/assets/grid.html new file mode 100644 index 0000000000..437193573d --- /dev/null +++ b/remote/test/puppeteer/test/assets/grid.html @@ -0,0 +1,51 @@ +<script> +document.addEventListener('DOMContentLoaded', function() { + function generatePalette(amount) { + var result = []; + var hueStep = 360 / amount; + for (var i = 0; i < amount; ++i) + result.push('hsl(' + (hueStep * i) + ', 100%, 90%)'); + return result; + } + + var palette = generatePalette(100); + for (var i = 0; i < 200; ++i) { + var box = document.createElement('div'); + box.classList.add('box'); + box.style.setProperty('background-color', palette[i % palette.length]); + var x = i; + do { + var digit = x % 10; + x = (x / 10)|0; + var img = document.createElement('img'); + img.src = `./digits/${digit}.png`; + box.insertBefore(img, box.firstChild); + } while (x); + document.body.appendChild(box); + } +}); +</script> + +<style> + +:root { + scrollbar-width: none; +} + +body { + margin: 0; + padding: 0; +} + +.box { + font-family: arial; + display: inline-flex; + align-items: center; + justify-content: center; + margin: 0; + padding: 0; + width: 50px; + height: 50px; + box-sizing: border-box; + border: 1px solid darkgray; +} diff --git a/remote/test/puppeteer/test/assets/historyapi.html b/remote/test/puppeteer/test/assets/historyapi.html new file mode 100644 index 0000000000..bacaf9e9a0 --- /dev/null +++ b/remote/test/puppeteer/test/assets/historyapi.html @@ -0,0 +1,5 @@ +<script> +window.addEventListener('DOMContentLoaded', () => { + history.pushState({}, '', '#1'); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/idle-detector.html b/remote/test/puppeteer/test/assets/idle-detector.html new file mode 100644 index 0000000000..83b496c03d --- /dev/null +++ b/remote/test/puppeteer/test/assets/idle-detector.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<div id="state"></div> +<script> + const elState = document.querySelector('#state'); + function setState(msg) { + elState.textContent = msg; + } + async function main() { + const controller = new AbortController(); + const signal = controller.signal; + const idleDetector = new IdleDetector({ + threshold: 60000, + signal, + }); + idleDetector.addEventListener('change', () => { + const userState = idleDetector.userState; + const screenState = idleDetector.screenState; + setState(`Idle state: ${userState}, ${screenState}.`); + }); + idleDetector.start(); + } + main(); +</script> diff --git a/remote/test/puppeteer/test/assets/initiator.html b/remote/test/puppeteer/test/assets/initiator.html new file mode 100644 index 0000000000..12889d3242 --- /dev/null +++ b/remote/test/puppeteer/test/assets/initiator.html @@ -0,0 +1,2 @@ +<iframe src="./frames/frame.html"></iframe> +<script src="./initiator.js"></script> diff --git a/remote/test/puppeteer/test/assets/initiator.js b/remote/test/puppeteer/test/assets/initiator.js new file mode 100644 index 0000000000..642e775f31 --- /dev/null +++ b/remote/test/puppeteer/test/assets/initiator.js @@ -0,0 +1,8 @@ +const script = document.createElement('script'); +script.src = './injectedfile.js'; +document.body.appendChild(script); + +const style = document.createElement('link'); +style.rel = 'stylesheet'; +style.href = './injectedstyle.css'; +document.head.appendChild(style); diff --git a/remote/test/puppeteer/test/assets/injectedfile.js b/remote/test/puppeteer/test/assets/injectedfile.js new file mode 100644 index 0000000000..c211b62c16 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedfile.js @@ -0,0 +1,2 @@ +window.__injected = 42; +window.__injectedError = new Error('hi'); diff --git a/remote/test/puppeteer/test/assets/injectedstyle.css b/remote/test/puppeteer/test/assets/injectedstyle.css new file mode 100644 index 0000000000..aa1634c255 --- /dev/null +++ b/remote/test/puppeteer/test/assets/injectedstyle.css @@ -0,0 +1,3 @@ +body { + background-color: red; +} diff --git a/remote/test/puppeteer/test/assets/inline-svg.html b/remote/test/puppeteer/test/assets/inline-svg.html new file mode 100644 index 0000000000..20023ecc79 --- /dev/null +++ b/remote/test/puppeteer/test/assets/inline-svg.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <body> + <svg> + <circle cx="10" cy="10" r="10" /> + </svg> + + <div style="margin-top: 5000px;"> + <svg> + <circle cx="10" cy="10" r="10" /> + </svg> + </div> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/inner-frame1.html b/remote/test/puppeteer/test/assets/inner-frame1.html new file mode 100644 index 0000000000..00f19ec166 --- /dev/null +++ b/remote/test/puppeteer/test/assets/inner-frame1.html @@ -0,0 +1,10 @@ +<script> + window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = 'inner-frame2.test'; + url.pathname = '/inner-frame2.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); + }, false); +</script> diff --git a/remote/test/puppeteer/test/assets/inner-frame2.html b/remote/test/puppeteer/test/assets/inner-frame2.html new file mode 100644 index 0000000000..9a236cc48f --- /dev/null +++ b/remote/test/puppeteer/test/assets/inner-frame2.html @@ -0,0 +1 @@ +<button>click</button> diff --git a/remote/test/puppeteer/test/assets/input/button.html b/remote/test/puppeteer/test/assets/input/button.html new file mode 100644 index 0000000000..d4c6e13fd2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/button.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <title>Button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/checkbox.html b/remote/test/puppeteer/test/assets/input/checkbox.html new file mode 100644 index 0000000000..ca56762e2b --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/checkbox.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <label for="agree">Remember Me</label> + <input id="agree" type="checkbox"> + <script> + window.result = { + check: null, + events: [], + }; + + let checkbox = document.querySelector('input'); + + const events = [ + 'change', + 'click', + 'dblclick', + 'input', + 'mousedown', + 'mouseenter', + 'mouseleave', + 'mousemove', + 'mouseout', + 'mouseover', + 'mouseup', + ]; + + for (let event of events) { + checkbox.addEventListener(event, () => { + if (['change', 'click', 'dblclick', 'input'].includes(event) === true) { + result.check = checkbox.checked; + } + + result.events.push(event); + }, false); + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/drag-and-drop.html b/remote/test/puppeteer/test/assets/input/drag-and-drop.html new file mode 100644 index 0000000000..b77870c4ad --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/drag-and-drop.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> + <head> + <title>Drag-and-drop test</title> + <style> + #drop { + width: 5em; + height: 5em; + border: 1px solid black; + } + </style> + </head> + <body> + <div id="drag" draggable="true">drag me</div> + <div id="drop"></div> + <div id="drag-state">0</div> + <script> + const drag = document.getElementById('drag'); + const drop = document.getElementById('drop'); + drag.addEventListener('dragstart', function(event) { + event.dataTransfer.setData('id', event.target.id); + document.getElementById('drag-state').textContent += '1'; + }); + drop.addEventListener('dragenter', function(event) { + event.preventDefault(); + document.getElementById('drag-state').textContent += '2'; + }); + drop.addEventListener('dragover', function(event) { + event.preventDefault(); + document.getElementById('drag-state').textContent += '3'; + }); + drop.addEventListener('drop', function(event) { + event.preventDefault(); + const id = event.dataTransfer.getData('id'); + const el = document.getElementById(id); + if (el) { + event.target.appendChild(el); + document.getElementById('drag-state').textContent += '4'; + } + }); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/fileupload.html b/remote/test/puppeteer/test/assets/input/fileupload.html new file mode 100644 index 0000000000..55fd7c5006 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/fileupload.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>File upload test</title> + </head> + <body> + <input type="file"> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/keyboard.html b/remote/test/puppeteer/test/assets/input/keyboard.html new file mode 100644 index 0000000000..2f4b7d33c2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/keyboard.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <title>Keyboard test</title> + </head> + <body> + <textarea></textarea> + <script> + window.result = ""; + let textarea = document.querySelector('textarea'); + textarea.focus(); + textarea.addEventListener('keydown', event => { + log('Keydown:', event.key, event.code, modifiers(event)); + }); + textarea.addEventListener('input', event => { + log('input:', event.data, event.inputType, event.isComposing); + }); + textarea.addEventListener('keyup', event => { + log('Keyup:', event.key, event.code, modifiers(event)); + }); + function modifiers(event) { + let m = []; + if (event.altKey) + m.push('Alt') + if (event.ctrlKey) + m.push('Control'); + if (event.shiftKey) + m.push('Shift') + return '[' + m.join(' ') + ']'; + } + function log(...args) { + console.log.apply(console, args); + result += args.join(' ') + '\n'; + } + function getResult() { + let temp = result.trim(); + result = ""; + return temp; + } + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/input/mouse-helper.js b/remote/test/puppeteer/test/assets/input/mouse-helper.js new file mode 100644 index 0000000000..97a764aa80 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/mouse-helper.js @@ -0,0 +1,74 @@ +// This injects a box into the page that moves with the mouse; +// Useful for debugging +(function () { + const box = document.createElement('div'); + box.classList.add('mouse-helper'); + const styleElement = document.createElement('style'); + styleElement.innerHTML = ` + .mouse-helper { + pointer-events: none; + position: absolute; + top: 0; + left: 0; + width: 20px; + height: 20px; + background: rgba(0,0,0,.4); + border: 1px solid white; + border-radius: 10px; + margin-left: -10px; + margin-top: -10px; + transition: background .2s, border-radius .2s, border-color .2s; + } + .mouse-helper.button-1 { + transition: none; + background: rgba(0,0,0,0.9); + } + .mouse-helper.button-2 { + transition: none; + border-color: rgba(0,0,255,0.9); + } + .mouse-helper.button-3 { + transition: none; + border-radius: 4px; + } + .mouse-helper.button-4 { + transition: none; + border-color: rgba(255,0,0,0.9); + } + .mouse-helper.button-5 { + transition: none; + border-color: rgba(0,255,0,0.9); + } + `; + document.head.appendChild(styleElement); + document.body.appendChild(box); + document.addEventListener( + 'mousemove', + (event) => { + box.style.left = event.pageX + 'px'; + box.style.top = event.pageY + 'px'; + updateButtons(event.buttons); + }, + true + ); + document.addEventListener( + 'mousedown', + (event) => { + updateButtons(event.buttons); + box.classList.add('button-' + event.which); + }, + true + ); + document.addEventListener( + 'mouseup', + (event) => { + updateButtons(event.buttons); + box.classList.remove('button-' + event.which); + }, + true + ); + function updateButtons(buttons) { + for (let i = 0; i < 5; i++) + {box.classList.toggle('button-' + i, buttons & (1 << i));} + } +})(); diff --git a/remote/test/puppeteer/test/assets/input/rotatedButton.html b/remote/test/puppeteer/test/assets/input/rotatedButton.html new file mode 100644 index 0000000000..1bce66cf5e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/rotatedButton.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> + <head> + <title>Rotated button test</title> + </head> + <body> + <script src="mouse-helper.js"></script> + <button onclick="clicked();">Click target</button> + <style> + button { + transform: rotateY(180deg); + } + </style> + <script> + window.result = 'Was not clicked'; + function clicked() { + result = 'Clicked'; + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/scrollable.html b/remote/test/puppeteer/test/assets/input/scrollable.html new file mode 100644 index 0000000000..75757824a4 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/scrollable.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + <head> + <title>Scrollable test</title> + </head> + <body> + <script src='mouse-helper.js'></script> + <script> + for (let i = 0; i < 100; i++) { + let button = document.createElement('button'); + button.textContent = i + ': not clicked'; + button.id = 'button-' + i; + button.onclick = () => button.textContent = 'clicked'; + button.oncontextmenu = event => { + if (![2].includes(event.button)) { + return; + } + event.preventDefault(); + button.textContent = 'context menu'; + } + button.onmouseup = event => { + if (![1,3,4].includes(event.button)) { + return; + } + event.preventDefault(); + button.textContent = { + 3: 'back click', + 4: 'forward click', + 1: 'aux click', + }[event.button]; + } + document.body.appendChild(button); + document.body.appendChild(document.createElement('br')); + } + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/select.html b/remote/test/puppeteer/test/assets/input/select.html new file mode 100644 index 0000000000..026d48e328 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/select.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html> + <head> + <title>Selection Test</title> + </head> + <body> + <select> + <option value="">Empty</option> + <option value="black">Black</option> + <option value="blue">Blue</option> + <option value="brown">Brown</option> + <option value="cyan">Cyan</option> + <option value="gray">Gray</option> + <option value="green">Green</option> + <option value="indigo">Indigo</option> + <option value="magenta">Magenta</option> + <option value="orange">Orange</option> + <option value="pink">Pink</option> + <option value="purple">Purple</option> + <option value="red">Red</option> + <option value="violet">Violet</option> + <option value="white">White</option> + <option value="yellow">Yellow</option> + </select> + <script> + window.result = { + onInput: null, + onChange: null, + onBubblingChange: null, + onBubblingInput: null, + }; + + let select = document.querySelector('select'); + + function makeEmpty() { + for (let i = select.options.length - 1; i >= 0; --i) { + select.remove(i); + } + } + + function makeMultiple() { + select.setAttribute('multiple', true); + } + + select.addEventListener('input', () => { + result.onInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + select.addEventListener('change', () => { + result.onChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('input', () => { + result.onBubblingInput = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + + document.body.addEventListener('change', () => { + result.onBubblingChange = Array.from(select.querySelectorAll('option:checked')).map((option) => { + return option.value; + }); + }, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/textarea.html b/remote/test/puppeteer/test/assets/input/textarea.html new file mode 100644 index 0000000000..66fdc40304 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/textarea.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Textarea test</title> + </head> + <body> + <textarea rows="5" cols="20"></textarea> + <script src='mouse-helper.js'></script> + <script> + globalThis.result = ''; + globalThis.textarea = document.querySelector('textarea'); + textarea.addEventListener('input', () => result = textarea.value, false); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/touchscreen.html b/remote/test/puppeteer/test/assets/input/touchscreen.html new file mode 100644 index 0000000000..76e31c97f9 --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/touchscreen.html @@ -0,0 +1,122 @@ +<!doctype html> +<html> + <head> + <title>Touch test</title> + </head> + + <body> + <style> + button { + box-sizing: border-box; + position: absolute; + left: 0; + top: 0; + width: 10px; + height: 10px; + padding: 0; + margin: 0; + } + </style> + <button>Click target</button> + <script> + var allEvents = []; + globalThis.addEventListener( + "touchstart", + (event) => { + allEvents.push({ + type: "touchstart", + touches: [...event.changedTouches].map((touch) => [ + touch.clientX, + touch.clientY, + touch.radiusX, + touch.radiusY, + ]), + }); + }, + true, + ); + globalThis.addEventListener( + "touchmove", + (event) => { + allEvents.push({ + type: "touchmove", + touches: [...event.changedTouches].map((touch) => [ + touch.clientX, + touch.clientY, + touch.radiusX, + touch.radiusY, + ]), + }); + }, + true, + ); + globalThis.addEventListener( + "touchend", + (event) => { + allEvents.push({ + type: "touchend", + touches: [...event.changedTouches].map((touch) => [ + touch.clientX, + touch.clientY, + touch.radiusX, + touch.radiusY, + ]) + }); + }, + true, + ); + globalThis.addEventListener( + "pointerdown", + (event) => { + allEvents.push({ + type: "pointerdown", + x: event.x, + y: event.y, + width: event.width, + height: event.height, + }); + }, + true, + ); + globalThis.addEventListener( + "pointermove", + (event) => { + allEvents.push({ + type: "pointermove", + x: event.x, + y: event.y, + width: event.width, + height: event.height, + }); + }, + true, + ); + globalThis.addEventListener( + "pointerup", + (event) => { + allEvents.push({ + type: "pointerup", + x: event.x, + y: event.y, + width: event.width, + height: event.height, + }); + }, + true, + ); + globalThis.addEventListener( + "click", + (event) => { + allEvents.push({ + type: "click", + x: event.x, + y: event.y, + width: event.width, + height: event.height, + }); + }, + true, + ); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/input/wheel.html b/remote/test/puppeteer/test/assets/input/wheel.html new file mode 100644 index 0000000000..3d093a993e --- /dev/null +++ b/remote/test/puppeteer/test/assets/input/wheel.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <style> + body { + min-height: 100vh; + margin: 0; + display: flex; + align-items: center; + justify-content: center; + } + + div { + width: 105px; + height: 105px; + background: #cdf; + padding: 5px; + } + </style> + <title>Element: wheel event - Scaling_an_element_via_the_wheel - code sample</title> + </head> + <body> + <div>Scale me with your mouse wheel.</div> + <script> + function zoom(event) { + event.preventDefault(); + + scale += event.deltaY * -0.01; + + // Restrict scale + scale = Math.min(Math.max(.125, scale), 4); + + // Apply scale transform + el.style.transform = `scale(${scale})`; + } + + let scale = 1; + const el = document.querySelector('div'); + el.onwheel = zoom; + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/jscoverage/eval.html b/remote/test/puppeteer/test/assets/jscoverage/eval.html new file mode 100644 index 0000000000..838ae28763 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ +<script>eval('console.log("foo")')</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/involved.html b/remote/test/puppeteer/test/assets/jscoverage/involved.html new file mode 100644 index 0000000000..fcc32ba2ca --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/involved.html @@ -0,0 +1,16 @@ +<script> +function foo() { + if (1 > 2) + console.log(1); + if (1 < 2) + console.log(2); + let x = 1 > 2 ? 'foo' : 'bar'; + let y = 1 < 2 ? 'foo' : 'bar'; + let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}}; + let z = () => {}; + let q = () => {}; + q(); +} + +foo(); +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/multiple.html b/remote/test/puppeteer/test/assets/jscoverage/multiple.html new file mode 100644 index 0000000000..bdef59885b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/multiple.html @@ -0,0 +1,2 @@ +<script src='script1.js'></script> +<script src='script2.js'></script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/ranges.html b/remote/test/puppeteer/test/assets/jscoverage/ranges.html new file mode 100644 index 0000000000..3d02670aea --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/ranges.html @@ -0,0 +1,2 @@ +<script> +function unused(){}console.log('used!');if(true===false)console.log('unused!');</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/script1.js b/remote/test/puppeteer/test/assets/jscoverage/script1.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script1.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/script2.js b/remote/test/puppeteer/test/assets/jscoverage/script2.js new file mode 100644 index 0000000000..3bd241b50e --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/script2.js @@ -0,0 +1 @@ +console.log(3); diff --git a/remote/test/puppeteer/test/assets/jscoverage/simple.html b/remote/test/puppeteer/test/assets/jscoverage/simple.html new file mode 100644 index 0000000000..49eeeea6ae --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/simple.html @@ -0,0 +1,2 @@ +<script> +function foo() {function bar() { } console.log(1); } foo(); </script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html new file mode 100644 index 0000000000..e477750320 --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/sourceurl.html @@ -0,0 +1,4 @@ +<script> +console.log(1); +//# sourceURL=nicename.js +</script> diff --git a/remote/test/puppeteer/test/assets/jscoverage/unused.html b/remote/test/puppeteer/test/assets/jscoverage/unused.html new file mode 100644 index 0000000000..59c4a5a70b --- /dev/null +++ b/remote/test/puppeteer/test/assets/jscoverage/unused.html @@ -0,0 +1 @@ +<script>function foo() { }</script> diff --git a/remote/test/puppeteer/test/assets/lazy-oopif-frame.html b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html new file mode 100644 index 0000000000..83a420d029 --- /dev/null +++ b/remote/test/puppeteer/test/assets/lazy-oopif-frame.html @@ -0,0 +1,3 @@ +<iframe width="100%" height="300" src="about:blank"></iframe> +<div style="height: 800vh"></div> +<iframe width="100%" height="300" src="https://www.example.com" loading="lazy"></iframe> diff --git a/remote/test/puppeteer/test/assets/main-frame.html b/remote/test/puppeteer/test/assets/main-frame.html new file mode 100644 index 0000000000..0c50feff85 --- /dev/null +++ b/remote/test/puppeteer/test/assets/main-frame.html @@ -0,0 +1,10 @@ +<script> + window.addEventListener('DOMContentLoaded', () => { + const iframe = document.createElement('iframe'); + const url = new URL(location.href); + url.hostname = 'inner-frame1.test'; + url.pathname = '/inner-frame1.html'; + iframe.src = url.toString(); + document.body.appendChild(iframe); + }, false); +</script> diff --git a/remote/test/puppeteer/test/assets/mobile.html b/remote/test/puppeteer/test/assets/mobile.html new file mode 100644 index 0000000000..8e94b2fe29 --- /dev/null +++ b/remote/test/puppeteer/test/assets/mobile.html @@ -0,0 +1 @@ +<meta name = "viewport" content = "initial-scale = 1, user-scalable = no"> diff --git a/remote/test/puppeteer/test/assets/modernizr.js b/remote/test/puppeteer/test/assets/modernizr.js new file mode 100644 index 0000000000..7991a4ec40 --- /dev/null +++ b/remote/test/puppeteer/test/assets/modernizr.js @@ -0,0 +1,3 @@ +/*! modernizr 3.5.0 (Custom Build) | MIT * +* https://modernizr.com/download/?-touchevents-setclasses !*/ +!function(e,n,t){function o(e,n){return typeof e===n}function s(){var e,n,t,s,a,i,r;for(var l in c)if(c.hasOwnProperty(l)){if(e=[],n=c[l],n.name&&(e.push(n.name.toLowerCase()),n.options&&n.options.aliases&&n.options.aliases.length))for(t=0;t<n.options.aliases.length;t++)e.push(n.options.aliases[t].toLowerCase());for(s=o(n.fn,"function")?n.fn():n.fn,a=0;a<e.length;a++)i=e[a],r=i.split("."),1===r.length?Modernizr[r[0]]=s:(!Modernizr[r[0]]||Modernizr[r[0]]instanceof Boolean||(Modernizr[r[0]]=new Boolean(Modernizr[r[0]])),Modernizr[r[0]][r[1]]=s),f.push((s?"":"no-")+r.join("-"))}}function a(e){var n=u.className,t=Modernizr._config.classPrefix||"";if(p&&(n=n.baseVal),Modernizr._config.enableJSClass){var o=new RegExp("(^|\\s)"+t+"no-js(\\s|$)");n=n.replace(o,"$1"+t+"js$2")}Modernizr._config.enableClasses&&(n+=" "+t+e.join(" "+t),p?u.className.baseVal=n:u.className=n)}function i(){return"function"!=typeof n.createElement?n.createElement(arguments[0]):p?n.createElementNS.call(n,"http://www.w3.org/2000/svg",arguments[0]):n.createElement.apply(n,arguments)}function r(){var e=n.body;return e||(e=i(p?"svg":"body"),e.fake=!0),e}function l(e,t,o,s){var a,l,f,c,d="modernizr",p=i("div"),h=r();if(parseInt(o,10))for(;o--;)f=i("div"),f.id=s?s[o]:d+(o+1),p.appendChild(f);return a=i("style"),a.type="text/css",a.id="s"+d,(h.fake?h:p).appendChild(a),h.appendChild(p),a.styleSheet?a.styleSheet.cssText=e:a.appendChild(n.createTextNode(e)),p.id=d,h.fake&&(h.style.background="",h.style.overflow="hidden",c=u.style.overflow,u.style.overflow="hidden",u.appendChild(h)),l=t(p,e),h.fake?(h.parentNode.removeChild(h),u.style.overflow=c,u.offsetHeight):p.parentNode.removeChild(p),!!l}var f=[],c=[],d={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,n){var t=this;setTimeout(function(){n(t[e])},0)},addTest:function(e,n,t){c.push({name:e,fn:n,options:t})},addAsyncTest:function(e){c.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=d,Modernizr=new Modernizr;var u=n.documentElement,p="svg"===u.nodeName.toLowerCase(),h=d._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];d._prefixes=h;var m=d.testStyles=l;Modernizr.addTest("touchevents",function(){var t;if("ontouchstart"in e||e.DocumentTouch&&n instanceof DocumentTouch)t=!0;else{var o=["@media (",h.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");m(o,function(e){t=9===e.offsetTop})}return t}),s(),a(f),delete d.addTest,delete d.addAsyncTest;for(var v=0;v<Modernizr._q.length;v++)Modernizr._q[v]();e.Modernizr=Modernizr}(window,document); diff --git a/remote/test/puppeteer/test/assets/networkidle.html b/remote/test/puppeteer/test/assets/networkidle.html new file mode 100644 index 0000000000..910ae1736d --- /dev/null +++ b/remote/test/puppeteer/test/assets/networkidle.html @@ -0,0 +1,19 @@ +<script> + async function sleep(delay) { + return new Promise(resolve => setTimeout(resolve, delay)); + } + + async function main() { + const roundOne = Promise.all([ + fetch('fetch-request-a.js'), + fetch('fetch-request-b.js'), + fetch('fetch-request-c.js'), + ]); + + await roundOne; + await sleep(50); + await fetch('fetch-request-d.js'); + } + + main(); +</script> diff --git a/remote/test/puppeteer/test/assets/offscreenbuttons.html b/remote/test/puppeteer/test/assets/offscreenbuttons.html new file mode 100644 index 0000000000..e487caf4d3 --- /dev/null +++ b/remote/test/puppeteer/test/assets/offscreenbuttons.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<style> + button { + position: absolute; + width: 100px; + height: 20px; + } + + #btn0 { right: 0px; top: 0; } + #btn1 { right: -10px; top: 25px; } + #btn2 { right: -20px; top: 50px; } + #btn3 { right: -30px; top: 75px; } + #btn4 { right: -40px; top: 100px; } + #btn5 { right: -50px; top: 125px; } + #btn6 { right: -60px; top: 150px; } + #btn7 { right: -70px; top: 175px; } + #btn8 { right: -80px; top: 200px; } + #btn9 { right: -90px; top: 225px; } + #btn10 { right: -100px; top: 250px; } + #btn11 { right: -99.999px; top: 275px; } +</style> +<button id=btn0>0</button> +<button id=btn1>1</button> +<button id=btn2>2</button> +<button id=btn3>3</button> +<button id=btn4>4</button> +<button id=btn5>5</button> +<button id=btn6>6</button> +<button id=btn7>7</button> +<button id=btn8>8</button> +<button id=btn9>9</button> +<button id=btn10>10</button> +<button id=btn11>11</button> +<script> + for (const button of document.querySelectorAll('button')) { + button.addEventListener('click', () => { + console.log(`button #${button.textContent} clicked`); + }); + } +</script> diff --git a/remote/test/puppeteer/test/assets/one-style.css b/remote/test/puppeteer/test/assets/one-style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/one-style.html b/remote/test/puppeteer/test/assets/one-style.html new file mode 100644 index 0000000000..4760f2b9f7 --- /dev/null +++ b/remote/test/puppeteer/test/assets/one-style.html @@ -0,0 +1,2 @@ +<link rel='stylesheet' href='./one-style.css'> +<div>hello, world!</div> diff --git a/remote/test/puppeteer/test/assets/oopif.html b/remote/test/puppeteer/test/assets/oopif.html new file mode 100644 index 0000000000..f04b9127af --- /dev/null +++ b/remote/test/puppeteer/test/assets/oopif.html @@ -0,0 +1,5 @@ +<a id="navigate-within-document" href="#nav">Navigate within document</a> +<a name="nav"></a> +<script> + fetch('oopif.html?requestFromOOPIF') +</script> diff --git a/remote/test/puppeteer/test/assets/p-selectors.html b/remote/test/puppeteer/test/assets/p-selectors.html new file mode 100644 index 0000000000..24900623d8 --- /dev/null +++ b/remote/test/puppeteer/test/assets/p-selectors.html @@ -0,0 +1,15 @@ +<div id="a">hello <button id="b">world</button> + <span id="f"></span> + <div id="c"></div> +</div> +<a>My name is Jun (pronounced like "June")</a> + +<script> + const topShadow = document.querySelector('#c'); + topShadow.attachShadow({ mode: "open" }); + topShadow.shadowRoot.innerHTML = `shadow dom<div id="d"></div>`; + + const innerShadow = topShadow.shadowRoot.querySelector('#d'); + innerShadow.attachShadow({ mode: "open" }); + innerShadow.shadowRoot.innerHTML = `<a id="e">deep text</a>`; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/pdf.html b/remote/test/puppeteer/test/assets/pdf.html new file mode 100644 index 0000000000..987df27ebe --- /dev/null +++ b/remote/test/puppeteer/test/assets/pdf.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>PDF</title> + </head> + <body> + <div>PDF Content</div> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/picture.html b/remote/test/puppeteer/test/assets/picture.html new file mode 100644 index 0000000000..18e3d70f5e --- /dev/null +++ b/remote/test/puppeteer/test/assets/picture.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<img + srcset="logo-1x.png, logo-2x.png 2x, logo-3x.png 3x" + src="logo-1x.png" + height="320" + width="320" />
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/playground.html b/remote/test/puppeteer/test/assets/playground.html new file mode 100644 index 0000000000..828cfb1c70 --- /dev/null +++ b/remote/test/puppeteer/test/assets/playground.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> + <head> + <title>Playground</title> + </head> + <body> + <button>A button</button> + <textarea>A text area</textarea> + <div id="first">First div</div> + <div id="second"> + Second div + <span class="inner">Inner span</span> + </div> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/popup/popup.html b/remote/test/puppeteer/test/assets/popup/popup.html new file mode 100644 index 0000000000..b855162c25 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/popup.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup</title> + </head> + <body> + I am a popup + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/popup/window-open.html b/remote/test/puppeteer/test/assets/popup/window-open.html new file mode 100644 index 0000000000..d138be1d22 --- /dev/null +++ b/remote/test/puppeteer/test/assets/popup/window-open.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <title>Popup test</title> + </head> + <body> + <script> + window.open('./popup.html'); + </script> + </body> +</html> diff --git a/remote/test/puppeteer/test/assets/pptr.png b/remote/test/puppeteer/test/assets/pptr.png Binary files differnew file mode 100644 index 0000000000..65d87c68e6 --- /dev/null +++ b/remote/test/puppeteer/test/assets/pptr.png diff --git a/remote/test/puppeteer/test/assets/prerender/index.html b/remote/test/puppeteer/test/assets/prerender/index.html new file mode 100644 index 0000000000..e0eecb717d --- /dev/null +++ b/remote/test/puppeteer/test/assets/prerender/index.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<head> +<script> + function addRules() { + const script = document.createElement('script'); + script.type = 'speculationrules'; + script.innerText = ` + { + "prerender": [ + {"source": "list", "urls": ["target.html"]} + ] + } + `; + document.head.append(script); + } +</script> +</head> +<body> + <button onclick="addRules()">add rules</button> + <a href="target.html">test</a> +</body> diff --git a/remote/test/puppeteer/test/assets/prerender/target.html b/remote/test/puppeteer/test/assets/prerender/target.html new file mode 100644 index 0000000000..f384b3cbb0 --- /dev/null +++ b/remote/test/puppeteer/test/assets/prerender/target.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<head> + <script>fetch('target.html?fromPrerendered')</script> +</head> +<body>target<input></input></body> diff --git a/remote/test/puppeteer/test/assets/resetcss.html b/remote/test/puppeteer/test/assets/resetcss.html new file mode 100644 index 0000000000..e4e04b1f8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/resetcss.html @@ -0,0 +1,50 @@ +<style> +/* http://meyerweb.com/eric/tools/css/reset/ + v2.0 | 20110126 + License: none (public domain) +*/ + +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font: inherit; + vertical-align: baseline; +} +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, +footer, header, hgroup, menu, nav, section { + display: block; +} +body { + line-height: 1; +} +ol, ul { + list-style: none; +} +blockquote, q { + quotes: none; +} +blockquote:before, blockquote:after, +q:before, q:after { + content: ''; + content: none; +} +table { + border-collapse: collapse; + border-spacing: 0; +} +</style> diff --git a/remote/test/puppeteer/test/assets/resolution.html b/remote/test/puppeteer/test/assets/resolution.html new file mode 100644 index 0000000000..6d9f59ef9f --- /dev/null +++ b/remote/test/puppeteer/test/assets/resolution.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<style> + p { + color: transparent; + } + @media (resolution: 1dppx) { + p { + font-size: 1px; + } + } + @media (resolution: 2dppx) { + p { + font-size: 2px; + } + } + @media (resolution: 3dppx) { + p { + font-size: 3px; + } + } + </style> + <p>Test</p> +
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/self-request.html b/remote/test/puppeteer/test/assets/self-request.html new file mode 100644 index 0000000000..88aff620ff --- /dev/null +++ b/remote/test/puppeteer/test/assets/self-request.html @@ -0,0 +1,5 @@ +<script> +var req = new XMLHttpRequest(); +req.open('GET', '/self-request.html'); +req.send(null); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html new file mode 100644 index 0000000000..bef85d985b --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.html @@ -0,0 +1,3 @@ +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/empty/sw.js diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js new file mode 100644 index 0000000000..8b1a393741 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/background.js @@ -0,0 +1 @@ +// empty diff --git a/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json new file mode 100644 index 0000000000..25828b6d2b --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/extension/manifest.json @@ -0,0 +1,9 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "service_worker": "background.js" + }, + "permissions": ["background", "activeTab"], + "manifest_version": 3 +} diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css new file mode 100644 index 0000000000..7b26410d8a --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/style.css @@ -0,0 +1,3 @@ +body { + background-color: pink; +} diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html new file mode 100644 index 0000000000..a9d28acb09 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.html @@ -0,0 +1,5 @@ +<link rel="stylesheet" href="./style.css"> +<script> + window.registrationPromise = navigator.serviceWorker.register('sw.js'); + window.activationPromise = new Promise(resolve => navigator.serviceWorker.oncontrollerchange = resolve); +</script> diff --git a/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js new file mode 100644 index 0000000000..21381484b6 --- /dev/null +++ b/remote/test/puppeteer/test/assets/serviceworkers/fetch/sw.js @@ -0,0 +1,7 @@ +self.addEventListener('fetch', (event) => { + event.respondWith(fetch(event.request)); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(clients.claim()); +}); diff --git a/remote/test/puppeteer/test/assets/shadow.html b/remote/test/puppeteer/test/assets/shadow.html new file mode 100644 index 0000000000..3796ca768c --- /dev/null +++ b/remote/test/puppeteer/test/assets/shadow.html @@ -0,0 +1,17 @@ +<script> + +let h1 = null; +window.button = null; +window.clicked = false; + +window.addEventListener('DOMContentLoaded', () => { + const shadowRoot = document.body.attachShadow({mode: 'open'}); + h1 = document.createElement('h1'); + h1.textContent = 'Hellow Shadow DOM v1'; + button = document.createElement('button'); + button.textContent = 'Click'; + button.addEventListener('click', () => clicked = true); + shadowRoot.appendChild(h1); + shadowRoot.appendChild(button); +}); +</script> diff --git a/remote/test/puppeteer/test/assets/simple-extension/content-script.js b/remote/test/puppeteer/test/assets/simple-extension/content-script.js new file mode 100644 index 0000000000..0fd83b90f1 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/content-script.js @@ -0,0 +1,2 @@ +console.log('hey from the content-script'); +self.thisIsTheContentScript = true; diff --git a/remote/test/puppeteer/test/assets/simple-extension/index.js b/remote/test/puppeteer/test/assets/simple-extension/index.js new file mode 100644 index 0000000000..a0bb3f4eae --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/index.js @@ -0,0 +1,2 @@ +// Mock script for background extension +window.MAGIC = 42; diff --git a/remote/test/puppeteer/test/assets/simple-extension/manifest.json b/remote/test/puppeteer/test/assets/simple-extension/manifest.json new file mode 100644 index 0000000000..da2cd082ed --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple-extension/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "Simple extension", + "version": "0.1", + "background": { + "scripts": ["index.js"] + }, + "content_scripts": [{ + "matches": ["<all_urls>"], + "css": [], + "js": ["content-script.js"] + }], + "permissions": ["background", "activeTab"], + "manifest_version": 2 +} diff --git a/remote/test/puppeteer/test/assets/simple.json b/remote/test/puppeteer/test/assets/simple.json new file mode 100644 index 0000000000..6d95903051 --- /dev/null +++ b/remote/test/puppeteer/test/assets/simple.json @@ -0,0 +1 @@ +{"foo": "bar"} diff --git a/remote/test/puppeteer/test/assets/tamperable.html b/remote/test/puppeteer/test/assets/tamperable.html new file mode 100644 index 0000000000..d027e97038 --- /dev/null +++ b/remote/test/puppeteer/test/assets/tamperable.html @@ -0,0 +1,3 @@ +<script> + window.result = window.injected; +</script>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/title.html b/remote/test/puppeteer/test/assets/title.html new file mode 100644 index 0000000000..88a86ce412 --- /dev/null +++ b/remote/test/puppeteer/test/assets/title.html @@ -0,0 +1 @@ +<title>Woof-Woof</title> diff --git a/remote/test/puppeteer/test/assets/worker/worker.html b/remote/test/puppeteer/test/assets/worker/worker.html new file mode 100644 index 0000000000..7de2d9fd9e --- /dev/null +++ b/remote/test/puppeteer/test/assets/worker/worker.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <title>Worker test</title> + </head> + <body> + <script> + var worker = new Worker('worker.js'); + worker.onmessage = function(message) { + console.log(message.data); + }; + </script> + </body> +</html>
\ No newline at end of file diff --git a/remote/test/puppeteer/test/assets/worker/worker.js b/remote/test/puppeteer/test/assets/worker/worker.js new file mode 100644 index 0000000000..0626f13e58 --- /dev/null +++ b/remote/test/puppeteer/test/assets/worker/worker.js @@ -0,0 +1,16 @@ +console.log('hello from the worker'); + +function workerFunction() { + return 'worker function result'; +} + +self.addEventListener('message', (event) => { + console.log('got this data: ' + event.data); +}); + +(async function () { + while (true) { + self.postMessage(workerFunction.toString()); + await new Promise((x) => setTimeout(x, 100)); + } +})(); diff --git a/remote/test/puppeteer/test/assets/wrappedlink.html b/remote/test/puppeteer/test/assets/wrappedlink.html new file mode 100644 index 0000000000..429b6e9156 --- /dev/null +++ b/remote/test/puppeteer/test/assets/wrappedlink.html @@ -0,0 +1,32 @@ +<style> +:root { + font-family: monospace; +} + +body { + display: flex; + align-items: center; + justify-content: center; +} + +div { + width: 10ch; + word-wrap: break-word; + border: 1px solid blue; + transform: rotate(33deg); + line-height: 8ch; + padding: 2ch; +} + +a { + margin-left: 7ch; +} +</style> +<div> + <a href='#clicked'>123321</a> +</div> +<script> + document.querySelector('a').addEventListener('click', () => { + window.__clicked = true; + }); +</script> diff --git a/remote/test/puppeteer/test/fixtures/closeme.js b/remote/test/puppeteer/test/fixtures/closeme.js new file mode 100644 index 0000000000..dbe798f70d --- /dev/null +++ b/remote/test/puppeteer/test/fixtures/closeme.js @@ -0,0 +1,5 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + console.log(browser.wsEndpoint()); +})(); diff --git a/remote/test/puppeteer/test/fixtures/dumpio.js b/remote/test/puppeteer/test/fixtures/dumpio.js new file mode 100644 index 0000000000..a16cf4d633 --- /dev/null +++ b/remote/test/puppeteer/test/fixtures/dumpio.js @@ -0,0 +1,10 @@ +(async () => { + const [, , puppeteerRoot, options] = process.argv; + const browser = await require(puppeteerRoot).launch(JSON.parse(options)); + const page = await browser.newPage(); + await page.evaluate(() => { + return console.error('message from dumpio'); + }); + await page.close(); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt new file mode 100644 index 0000000000..ecb1a6342f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/csscoverage-involved.txt @@ -0,0 +1,20 @@ +[ + { + "url": "http://localhost:<PORT>/csscoverage/involved.html", + "ranges": [ + { + "start": 149, + "end": 297 + }, + { + "start": 306, + "end": 323 + }, + { + "start": 327, + "end": 433 + } + ], + "text": "\n@charset \"utf-8\";\n@namespace svg url(http://www.w3.org/2000/svg);\n@font-face {\n font-family: \"Example Font\";\n src: url(\"./Dosis-Regular.ttf\");\n}\n\n#fluffy {\n border: 1px solid black;\n z-index: 1;\n /* -webkit-disabled-property: rgb(1, 2, 3) */\n -lol-cats: \"dogs\" /* non-existing property */\n}\n\n@media (min-width: 1px) {\n span {\n -webkit-border-radius: 10px;\n font-family: \"Example Font\";\n animation: 1s identifier;\n }\n}\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png Binary files differnew file mode 100644 index 0000000000..c53502031f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio1.png diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png Binary files differnew file mode 100644 index 0000000000..9d3e9fcc31 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio2.png diff --git a/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png Binary files differnew file mode 100644 index 0000000000..3349dbd0ac --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/device-pixel-ratio3.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..ff282e989b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..91a1cb8510 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png Binary files differnew file mode 100644 index 0000000000..7b01753b6a --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-2.png diff --git a/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png Binary files differnew file mode 100644 index 0000000000..b9b8b2922b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/grid-cell-3.png diff --git a/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt new file mode 100644 index 0000000000..016b30bde8 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/jscoverage-involved.txt @@ -0,0 +1,36 @@ +[ + { + "url": "http://localhost:<PORT>/jscoverage/involved.html", + "ranges": [ + { + "start": 0, + "end": 35 + }, + { + "start": 50, + "end": 100 + }, + { + "start": 107, + "end": 141 + }, + { + "start": 148, + "end": 168 + }, + { + "start": 203, + "end": 204 + }, + { + "start": 238, + "end": 251 + }, + { + "start": 259, + "end": 298 + } + ], + "text": "\nfunction foo() {\n if (1 > 2)\n console.log(1);\n if (1 < 2)\n console.log(2);\n let x = 1 > 2 ? 'foo' : 'bar';\n let y = 1 < 2 ? 'foo' : 'bar';\n let p = {a:1 > 2?function(){console.log('unused');}:function(){console.log('unused');}};\n let z = () => {};\n let q = () => {};\n q();\n}\n\nfoo();\n" + } +]
\ No newline at end of file diff --git a/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png Binary files differnew file mode 100644 index 0000000000..8595e0598e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/mock-binary-response.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..b010d1f87f --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png Binary files differnew file mode 100644 index 0000000000..d713d27943 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect-scale2.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..ac23b7de50 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..32e05bf05b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..cc8669d598 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..35c53377f9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..5fcdb92355 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..d0c05ba795 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..917dd48188 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png Binary files differnew file mode 100644 index 0000000000..edc01c1041 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage-2.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..d6d38217f7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png Binary files differnew file mode 100644 index 0000000000..7ec69d3040 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip-2.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..d7637631b7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..ecab61fe17 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-chrome/transparent.png b/remote/test/puppeteer/test/golden-chrome/transparent.png Binary files differnew file mode 100644 index 0000000000..1cf45d8688 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/transparent.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png Binary files differnew file mode 100644 index 0000000000..4d74aac44c --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-achromatopsia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png Binary files differnew file mode 100644 index 0000000000..78979425a9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-blurredVision.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png Binary files differnew file mode 100644 index 0000000000..79b4b0fa1b --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-deuteranopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png Binary files differnew file mode 100644 index 0000000000..bede7c1ed0 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-protanopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png Binary files differnew file mode 100644 index 0000000000..d5f6bbec2e --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/vision-deficiency-tritanopia.png diff --git a/remote/test/puppeteer/test/golden-chrome/white.jpg b/remote/test/puppeteer/test/golden-chrome/white.jpg Binary files differnew file mode 100644 index 0000000000..fb9070def3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-chrome/white.jpg diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png Binary files differnew file mode 100644 index 0000000000..8c814cf3f4 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio1.png diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png Binary files differnew file mode 100644 index 0000000000..a52579a1af --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio2.png diff --git a/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png Binary files differnew file mode 100644 index 0000000000..d43e08f4ad --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/device-pixel-ratio3.png diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png Binary files differnew file mode 100644 index 0000000000..2e671db41c --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-0.png diff --git a/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png Binary files differnew file mode 100644 index 0000000000..a2a61af3d3 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/grid-cell-1.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png Binary files differnew file mode 100644 index 0000000000..a6f69dd20a --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-odd-size.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png Binary files differnew file mode 100644 index 0000000000..5cce794edb --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect-scale2.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png Binary files differnew file mode 100644 index 0000000000..0a96e67f9a --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-clip-rect.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png Binary files differnew file mode 100644 index 0000000000..63956b2a7c --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-bounding-box.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png Binary files differnew file mode 100644 index 0000000000..f554b1d62c --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional-offset.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png Binary files differnew file mode 100644 index 0000000000..5f58502b49 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-fractional.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png Binary files differnew file mode 100644 index 0000000000..cc0eb7bfe4 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-larger-than-viewport.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png Binary files differnew file mode 100644 index 0000000000..fadcaa1207 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-padding-border.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png Binary files differnew file mode 100644 index 0000000000..0a78fb1ae7 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-rotate.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png Binary files differnew file mode 100644 index 0000000000..fadcaa1207 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-element-scrolled-into-view.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png Binary files differnew file mode 100644 index 0000000000..ac47ec83b1 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage-2.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png Binary files differnew file mode 100644 index 0000000000..ac47ec83b1 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-grid-fullpage.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png Binary files differnew file mode 100644 index 0000000000..f7c0830ba9 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip-2.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png Binary files differnew file mode 100644 index 0000000000..4c34e47fbd --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-offscreen-clip.png diff --git a/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png Binary files differnew file mode 100644 index 0000000000..f02ecae645 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/screenshot-sanity.png diff --git a/remote/test/puppeteer/test/golden-firefox/transparent.png b/remote/test/puppeteer/test/golden-firefox/transparent.png Binary files differnew file mode 100644 index 0000000000..1cf45d8688 --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/transparent.png diff --git a/remote/test/puppeteer/test/golden-firefox/white.jpg b/remote/test/puppeteer/test/golden-firefox/white.jpg Binary files differnew file mode 100644 index 0000000000..f04d7ec2ad --- /dev/null +++ b/remote/test/puppeteer/test/golden-firefox/white.jpg diff --git a/remote/test/puppeteer/test/installation/.mocharc.cjs b/remote/test/puppeteer/test/installation/.mocharc.cjs new file mode 100644 index 0000000000..5a797716e0 --- /dev/null +++ b/remote/test/puppeteer/test/installation/.mocharc.cjs @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @type {import('mocha').MochaOptions} + */ +module.exports = { + spec: ['build/**/*.spec.js'], + timeout: '240000ms', +}; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js new file mode 100644 index 0000000000..8f8fb329e7 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/imports.js @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import 'puppeteer-core'; +import 'puppeteer-core/internal/revisions.js'; +import 'puppeteer-core/lib/esm/puppeteer/revisions.js'; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js new file mode 100644 index 0000000000..4776d7e261 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/launch.js @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import puppeteer from 'puppeteer-core'; + +(async () => { + try { + await puppeteer.launch({ + product: '${product}', + executablePath: 'node', + }); + } catch (error) { + if (error.message.includes('Failed to launch the browser process')) { + process.exit(0); + } + console.error(error); + process.exit(1); + } +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs new file mode 100644 index 0000000000..f4276f2589 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer-core/requires.cjs @@ -0,0 +1,9 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +require('puppeteer-core'); +require('puppeteer-core/internal/revisions.js'); +require('puppeteer-core/lib/cjs/puppeteer/revisions.js'); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js new file mode 100644 index 0000000000..9e6ce241b2 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.js @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import puppeteer from 'puppeteer'; +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.$('aria/example'); + await page.screenshot({path: 'example.png'}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts new file mode 100644 index 0000000000..28396d0096 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/basic.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import puppeteer from 'puppeteer'; +(async () => { + const browser = await puppeteer.launch(); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.$('aria/example'); + await page.screenshot({path: 'example.png'}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js b/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js new file mode 100644 index 0000000000..3e1df93654 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/bidi.js @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import puppeteer from 'puppeteer'; +(async () => { + const browser = await puppeteer.launch({ + protocol: 'webDriverBiDi', + }); + const page = await browser.newPage(); + await page.goto('http://example.com'); + await page.$('h1'); + await page.screenshot({path: 'example.png'}); + await browser.close(); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs new file mode 100644 index 0000000000..64a7b96681 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/.puppeteerrc.cjs @@ -0,0 +1,8 @@ +const {join} = require('path'); + +/** + * @type {import("puppeteer").Configuration} + */ +module.exports = { + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +}; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts new file mode 100644 index 0000000000..5bcb82ffc8 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/configuration/puppeteer.config.ts @@ -0,0 +1,6 @@ +import {type Configuration} from 'puppeteer'; +import {join} from 'path'; + +export default { + cacheDirectory: join(__dirname, '.cache', 'puppeteer'), +} satisfies Configuration; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js new file mode 100644 index 0000000000..cd742bafd5 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/imports.js @@ -0,0 +1,10 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import 'puppeteer'; + +// Should still be reachable. +import 'puppeteer-core/internal/revisions.js'; diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js b/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js new file mode 100644 index 0000000000..39a0113de9 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/installCanary.js @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Browser, + detectBrowserPlatform, + install, + resolveBuildId, +} from '@puppeteer/browsers'; + +(async () => { + await install({ + cacheDir: process.env['PUPPETEER_CACHE_DIR'], + browser: Browser.CHROME, + buildId: await resolveBuildId( + Browser.CHROME, + detectBrowserPlatform(), + 'canary' + ), + }); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs new file mode 100644 index 0000000000..208eee9021 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/requires.cjs @@ -0,0 +1,10 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +require('puppeteer'); + +// Should still be reachable. +require('puppeteer-core/internal/revisions.js'); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js b/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js new file mode 100644 index 0000000000..a810e2aac2 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/trimCache.js @@ -0,0 +1,11 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import puppeteer from 'puppeteer'; + +(async () => { + await puppeteer.trimCache(); +})(); diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json b/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json new file mode 100644 index 0000000000..ce77dbf8d9 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + }, +} diff --git a/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js new file mode 100644 index 0000000000..30de2a4890 --- /dev/null +++ b/remote/test/puppeteer/test/installation/assets/puppeteer/webpack/webpack.config.js @@ -0,0 +1,16 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export default { + mode: 'production', + entry: './index.js', + target: 'node', + externals: 'typescript', + output: { + path: process.cwd(), + filename: 'bundle.js', + }, +}; diff --git a/remote/test/puppeteer/test/installation/package.json b/remote/test/puppeteer/test/installation/package.json new file mode 100644 index 0000000000..f5e804d99c --- /dev/null +++ b/remote/test/puppeteer/test/installation/package.json @@ -0,0 +1,50 @@ +{ + "name": "@puppeteer-test/installation", + "version": "latest", + "type": "module", + "private": true, + "scripts": { + "build": "wireit", + "clean": "../../tools/clean.js", + "test": "mocha" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "build:packages" + ], + "files": [ + "tsconfig.json", + "src/**" + ], + "output": [ + "build/**", + "tsconfig.tsbuildinfo" + ] + }, + "build:packages": { + "command": "npm pack --quiet --workspace puppeteer --workspace puppeteer-core --workspace @puppeteer/browsers", + "dependencies": [ + "../../packages/puppeteer:build", + "../../packages/puppeteer-core:build", + "../../packages/browsers:build" + ], + "files": [], + "output": [ + "puppeteer-*.tgz" + ] + } + }, + "files": [ + ".mocharc.cjs", + "puppeteer-*.tgz", + "build", + "assets" + ], + "dependencies": { + "glob": "10.3.10", + "mocha": "10.2.0" + } +} diff --git a/remote/test/puppeteer/test/installation/src/browsers.spec.ts b/remote/test/puppeteer/test/installation/src/browsers.spec.ts new file mode 100644 index 0000000000..0c91731455 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/browsers.spec.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import {spawnSync} from 'child_process'; + +import {configureSandbox} from './sandbox.js'; + +describe('`@puppeteer/browsers`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers'], + }); + + it('can launch CLI', async function () { + const result = spawnSync('npx', ['@puppeteer/browsers', '--help'], { + // npx is not found without the shell flag on Windows. + shell: process.platform === 'win32', + cwd: this.sandbox, + }); + assert.strictEqual(result.status, 0); + assert.ok( + result.stdout + .toString('utf-8') + .startsWith('@puppeteer/browsers <command>') + ); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/constants.ts b/remote/test/puppeteer/test/installation/src/constants.ts new file mode 100644 index 0000000000..2b66b792d5 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/constants.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {dirname, join, resolve} from 'path'; +import {fileURLToPath} from 'url'; + +import {globSync} from 'glob'; + +export const PUPPETEER_CORE_PACKAGE_PATH = resolve( + globSync('puppeteer-core-*.tgz')[0]! +); +export const PUPPETEER_BROWSERS_PACKAGE_PATH = resolve( + globSync('puppeteer-browsers-[0-9]*.tgz')[0]! +); +export const PUPPETEER_PACKAGE_PATH = resolve( + globSync('puppeteer-[0-9]*.tgz')[0]! +); +export const ASSETS_DIR = join( + dirname(fileURLToPath(import.meta.url)), + '..', + 'assets' +); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts new file mode 100644 index 0000000000..650cbc1832 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-cli.spec.ts @@ -0,0 +1,58 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import {spawnSync} from 'child_process'; +import {existsSync} from 'fs'; +import {readdir} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; + +describe('Puppeteer CLI', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + PUPPETEER_SKIP_DOWNLOAD: 'true', + }; + }, + }); + + it('can launch', async function () { + const result = spawnSync('npx', ['puppeteer', '--help'], { + // npx is not found without the shell flag on Windows. + shell: process.platform === 'win32', + cwd: this.sandbox, + }); + assert.strictEqual(result.status, 0); + assert.ok( + result.stdout.toString('utf-8').startsWith('puppeteer <command>') + ); + }); + + it('can download a browser', async function () { + assert.ok(!existsSync(join(this.sandbox, '.cache', 'puppeteer'))); + const result = spawnSync( + 'npx', + ['puppeteer', 'browsers', 'install', 'chrome'], + { + // npx is not found without the shell flag on Windows. + shell: process.platform === 'win32', + cwd: this.sandbox, + env: { + ...process.env, + PUPPETEER_CACHE_DIR: join(this.sandbox, '.cache', 'puppeteer'), + }, + } + ); + assert.strictEqual(result.status, 0); + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 1); + assert.equal(files[0], 'chrome'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts new file mode 100644 index 0000000000..1ed5511f6c --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-configuration.spec.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import {readdir, writeFile} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer` with configuration', () => { + describe('cjs', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + before: async cwd => { + await writeFile( + join(cwd, '.puppeteerrc.cjs'), + await readAsset('puppeteer', 'configuration', '.puppeteerrc.cjs') + ); + }, + }); + + it('evaluates', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 2); + assert(files.includes('chrome')); + assert(files.includes('chrome-headless-shell')); + + const script = await readAsset('puppeteer', 'basic.js'); + await this.runScript(script, 'mjs'); + }); + }); + + describe('ts', () => { + configureSandbox({ + dependencies: [ + '@puppeteer/browsers', + 'puppeteer-core', + 'puppeteer', + 'typescript', + ], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + before: async cwd => { + await writeFile( + join(cwd, 'puppeteer.config.ts'), + await readAsset('puppeteer', 'configuration', 'puppeteer.config.ts') + ); + }, + }); + + it('evaluates', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 2); + assert(files.includes('chrome')); + assert(files.includes('chrome-headless-shell')); + + const script = await readAsset('puppeteer', 'basic.js'); + await this.runScript(script, 'mjs'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts new file mode 100644 index 0000000000..9df19e1c85 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-core.spec.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer-core`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core'], + }); + + it('evaluates CommonJS', async function () { + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); + + for (const product of ['firefox', 'chrome']) { + it(`\`launch\` for \`${product}\` with a bad \`executablePath\``, async function () { + const script = (await readAsset('puppeteer-core', 'launch.js')).replace( + '${product}', + product + ); + await this.runScript(script, 'mjs'); + }); + } +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts new file mode 100644 index 0000000000..b599af01dc --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-firefox.spec.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import {readdir} from 'fs/promises'; +import {platform} from 'os'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +// Skipping this test on Windows as windows runners are much slower. +(platform() === 'win32' ? describe.skip : describe)( + '`puppeteer` with Firefox', + () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + PUPPETEER_PRODUCT: 'firefox', + }; + }, + }); + + describe('with CDP', () => { + it('evaluates CommonJS', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 1); + assert.equal(files[0], 'firefox'); + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); + }); + + describe('with WebDriverBiDi', () => { + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer', 'bidi.js'); + await this.runScript(script, 'mjs'); + }); + }); + } +); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts new file mode 100644 index 0000000000..fc8ff133fb --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-typescript.spec.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {readFile, writeFile} from 'fs/promises'; +import {platform} from 'os'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {execFile, readAsset} from './util.js'; + +// Skipping this test on Windows as windows runners are much slower. +(platform() === 'win32' ? describe.skip : describe)( + '`puppeteer` with TypeScript', + () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + devDependencies: ['typescript@4.7.4', '@types/node@16.3.3'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('should work', async function () { + // Write a Webpack configuration. + await writeFile( + join(this.sandbox, 'tsconfig.json'), + await readAsset('puppeteer', 'tsconfig.json') + ); + + // Write the source code. + await writeFile( + join(this.sandbox, 'index.ts'), + await readAsset('puppeteer', 'basic.ts') + ); + + // Compile. + await execFile('npx', ['tsc'], {cwd: this.sandbox, shell: true}); + + const script = await readFile(join(this.sandbox, 'index.js'), 'utf-8'); + + await this.runScript(script, 'cjs'); + }); + } +); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts new file mode 100644 index 0000000000..93902aec32 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer-webpack.spec.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {readFile, rm, writeFile} from 'fs/promises'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {execFile, readAsset} from './util.js'; + +describe('`puppeteer` with Webpack', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + devDependencies: ['webpack', 'webpack-cli'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates WebPack Bundles', async function () { + // Write a Webpack configuration. + await writeFile( + join(this.sandbox, 'webpack.config.mjs'), + await readAsset('puppeteer', 'webpack', 'webpack.config.js') + ); + + // Write the source code. + await writeFile( + join(this.sandbox, 'index.js'), + await readAsset('puppeteer', 'basic.js') + ); + + // Bundle. + await execFile('npx', ['webpack'], {cwd: this.sandbox, shell: true}); + + // Remove `node_modules` to test independence. + await rm('node_modules', {recursive: true, force: true}); + + const script = await readFile(join(this.sandbox, 'bundle.js'), 'utf-8'); + + await this.runScript(script, 'cjs'); + }); +}); diff --git a/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts new file mode 100644 index 0000000000..d7b8757284 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/puppeteer.spec.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; +import {readdirSync} from 'fs'; +import {readdir} from 'fs/promises'; +import {platform} from 'os'; +import {join} from 'path'; + +import {configureSandbox} from './sandbox.js'; +import {readAsset} from './util.js'; + +describe('`puppeteer`', () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates CommonJS', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 2); + assert(files.includes('chrome')); + assert(files.includes('chrome-headless-shell')); + + const script = await readAsset('puppeteer-core', 'requires.cjs'); + await this.runScript(script, 'cjs'); + }); + + it('evaluates ES modules', async function () { + const script = await readAsset('puppeteer-core', 'imports.js'); + await this.runScript(script, 'mjs'); + }); +}); + +// Skipping this test on Windows as windows runners are much slower. +(platform() === 'win32' ? describe.skip : describe)( + '`puppeteer` with PUPPETEER_DOWNLOAD_PATH', + () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_DOWNLOAD_PATH: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates', async function () { + const files = await readdir(join(this.sandbox, '.cache', 'puppeteer')); + assert.equal(files.length, 2); + assert(files.includes('chrome')); + assert(files.includes('chrome-headless-shell')); + + const script = await readAsset('puppeteer', 'basic.js'); + await this.runScript(script, 'mjs'); + }); + } +); + +// Skipping this test on Windows as windows runners are much slower. +(platform() === 'win32' ? describe.skip : describe)( + '`puppeteer` clears cache', + () => { + configureSandbox({ + dependencies: ['@puppeteer/browsers', 'puppeteer-core', 'puppeteer'], + env: cwd => { + return { + PUPPETEER_CACHE_DIR: join(cwd, '.cache', 'puppeteer'), + }; + }, + }); + + it('evaluates', async function () { + assert.equal( + readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length, + 1 + ); + + await this.runScript( + await readAsset('puppeteer', 'installCanary.js'), + 'mjs' + ); + + assert.equal( + readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length, + 2 + ); + + await this.runScript(await readAsset('puppeteer', 'trimCache.js'), 'mjs'); + + assert.equal( + readdirSync(join(this.sandbox, '.cache', 'puppeteer', 'chrome')).length, + 1 + ); + }); + } +); diff --git a/remote/test/puppeteer/test/installation/src/sandbox.ts b/remote/test/puppeteer/test/installation/src/sandbox.ts new file mode 100644 index 0000000000..fde30dfcf9 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/sandbox.ts @@ -0,0 +1,131 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import crypto from 'crypto'; +import {mkdtemp, rm, writeFile} from 'fs/promises'; +import {tmpdir} from 'os'; +import {join} from 'path'; + +import { + PUPPETEER_CORE_PACKAGE_PATH, + PUPPETEER_PACKAGE_PATH, + PUPPETEER_BROWSERS_PACKAGE_PATH, +} from './constants.js'; +import {execFile} from './util.js'; + +const PKG_MANAGER = process.env['PKG_MANAGER'] || 'npm'; + +let ADD_PKG_SUBCOMMAND = 'install'; +if (PKG_MANAGER !== 'npm') { + ADD_PKG_SUBCOMMAND = 'add'; +} + +export interface ItEvaluatesOptions { + commonjs?: boolean; +} + +export interface ItEvaluatesFn { + ( + title: string, + options: ItEvaluatesOptions, + getScriptContent: (cwd: string) => Promise<string> + ): void; + (title: string, getScriptContent: (cwd: string) => Promise<string>): void; +} + +export interface SandboxOptions { + dependencies?: string[]; + devDependencies?: string[]; + /** + * This should be idempotent. + */ + env?: ((cwd: string) => NodeJS.ProcessEnv) | NodeJS.ProcessEnv; + before?: (cwd: string) => Promise<void>; +} + +declare module 'mocha' { + export interface Context { + /** + * The path to the root of the sandbox folder. + */ + sandbox: string; + env: NodeJS.ProcessEnv | undefined; + runScript: (content: string, type: 'cjs' | 'mjs') => Promise<void>; + } +} + +/** + * Configures mocha before/after hooks to create a temp folder and install + * specified dependencies. + */ +export const configureSandbox = (options: SandboxOptions): void => { + before(async function (): Promise<void> { + console.time('before'); + const sandbox = await mkdtemp(join(tmpdir(), 'puppeteer-')); + const dependencies = (options.dependencies ?? []).map(module => { + switch (module) { + case 'puppeteer': + return PUPPETEER_PACKAGE_PATH; + case 'puppeteer-core': + return PUPPETEER_CORE_PACKAGE_PATH; + case '@puppeteer/browsers': + return PUPPETEER_BROWSERS_PACKAGE_PATH; + default: + return module; + } + }); + const devDependencies = options.devDependencies ?? []; + + let getEnv: (cwd: string) => NodeJS.ProcessEnv | undefined; + if (typeof options.env === 'function') { + getEnv = options.env; + } else { + const env = options.env; + getEnv = () => { + return env; + }; + } + const env = {...process.env, ...getEnv(sandbox)}; + + await options.before?.(sandbox); + if (dependencies.length > 0) { + await execFile(PKG_MANAGER, [ADD_PKG_SUBCOMMAND, ...dependencies], { + cwd: sandbox, + env, + shell: true, + }); + } + if (devDependencies.length > 0) { + await execFile( + PKG_MANAGER, + [ADD_PKG_SUBCOMMAND, '-D', ...devDependencies], + { + cwd: sandbox, + env, + shell: true, + } + ); + } + + this.sandbox = sandbox; + this.env = env; + this.runScript = async (content: string, type: 'cjs' | 'mjs') => { + const script = join(sandbox, `script-${crypto.randomUUID()}.${type}`); + await writeFile(script, content); + await execFile('node', [script], {cwd: sandbox, env}); + }; + console.timeEnd('before'); + }); + + after(async function () { + console.time('after'); + if (!process.env['KEEP_SANDBOX']) { + await rm(this.sandbox, {recursive: true, force: true, maxRetries: 5}); + } else { + console.log('sandbox saved in', this.sandbox); + } + console.timeEnd('after'); + }); +}; diff --git a/remote/test/puppeteer/test/installation/src/util.ts b/remote/test/puppeteer/test/installation/src/util.ts new file mode 100644 index 0000000000..c975fd61e3 --- /dev/null +++ b/remote/test/puppeteer/test/installation/src/util.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {execFile as execFileAsync} from 'child_process'; +import {readFile} from 'fs/promises'; +import {join} from 'path'; +import {promisify} from 'util'; + +import {ASSETS_DIR} from './constants.js'; + +export const execFile = promisify(execFileAsync); +export const readAsset = (...components: string[]): Promise<string> => { + return readFile(join(ASSETS_DIR, ...components), 'utf8'); +}; diff --git a/remote/test/puppeteer/test/installation/tsconfig.json b/remote/test/puppeteer/test/installation/tsconfig.json new file mode 100644 index 0000000000..146127b470 --- /dev/null +++ b/remote/test/puppeteer/test/installation/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "build", + "rootDir": "src", + }, + "include": ["src"], +} diff --git a/remote/test/puppeteer/test/installation/tsdoc.json b/remote/test/puppeteer/test/installation/tsdoc.json new file mode 100644 index 0000000000..f5b91f4af6 --- /dev/null +++ b/remote/test/puppeteer/test/installation/tsdoc.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + "tagDefinitions": [ + { + "tagName": "@license", + "syntaxKind": "modifier", + "allowMultiple": false + } + ], + "supportForTags": { + "@license": true + } +} diff --git a/remote/test/puppeteer/test/package.json b/remote/test/puppeteer/test/package.json new file mode 100644 index 0000000000..6470297572 --- /dev/null +++ b/remote/test/puppeteer/test/package.json @@ -0,0 +1,37 @@ +{ + "name": "@puppeteer-test/test", + "version": "latest", + "private": true, + "scripts": { + "build": "wireit", + "clean": "../tools/clean.js" + }, + "wireit": { + "build": { + "command": "tsc -b", + "clean": "if-file-deleted", + "dependencies": [ + "../packages/puppeteer:build", + "../packages/testserver:build" + ], + "files": [ + "src/**" + ], + "output": [ + "build/**", + "tsconfig.tsbuildinfo" + ] + } + }, + "dependencies": { + "diff": "5.1.0", + "jpeg-js": "0.4.4", + "pixelmatch": "5.3.0", + "pngjs": "7.0.0" + }, + "devDependencies": { + "@types/diff": "5.0.9", + "@types/pixelmatch": "5.2.6", + "@types/pngjs": "6.0.4" + } +} diff --git a/remote/test/puppeteer/test/src/accessibility.spec.ts b/remote/test/puppeteer/test/src/accessibility.spec.ts new file mode 100644 index 0000000000..09e9c90b96 --- /dev/null +++ b/remote/test/puppeteer/test/src/accessibility.spec.ts @@ -0,0 +1,567 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; + +import expect from 'expect'; +import type {SerializedAXNode} from 'puppeteer-core/internal/cdp/Accessibility.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Accessibility', function () { + setupTestBrowserHooks(); + + it('should work', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <head> + <title>Accessibility Test</title> + </head> + <body> + <div>Hello World</div> + <h1>Inputs</h1> + <input placeholder="Empty input" autofocus /> + <input placeholder="readonly input" readonly /> + <input placeholder="disabled input" disabled /> + <input aria-label="Input with whitespace" value=" " /> + <input value="value only" /> + <input aria-placeholder="placeholder" value="and a value" /> + <div aria-hidden="true" id="desc">This is a description!</div> + <input aria-placeholder="placeholder" value="and a value" aria-describedby="desc" /> + <select> + <option>First Option</option> + <option>Second Option</option> + </select> + </body>`); + + await page.focus('[placeholder="Empty input"]'); + const golden = isFirefox + ? { + role: 'document', + name: 'Accessibility Test', + children: [ + {role: 'text leaf', name: 'Hello World'}, + {role: 'heading', name: 'Inputs', level: 1}, + {role: 'entry', name: 'Empty input', focused: true}, + {role: 'entry', name: 'readonly input', readonly: true}, + {role: 'entry', name: 'disabled input', disabled: true}, + {role: 'entry', name: 'Input with whitespace', value: ' '}, + {role: 'entry', name: '', value: 'value only'}, + {role: 'entry', name: '', value: 'and a value'}, // firefox doesn't use aria-placeholder for the name + { + role: 'entry', + name: '', + value: 'and a value', + description: 'This is a description!', + }, // and here + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: true, + children: [ + { + role: 'combobox option', + name: 'First Option', + selected: true, + }, + {role: 'combobox option', name: 'Second Option'}, + ], + }, + ], + } + : { + role: 'RootWebArea', + name: 'Accessibility Test', + children: [ + {role: 'StaticText', name: 'Hello World'}, + {role: 'heading', name: 'Inputs', level: 1}, + {role: 'textbox', name: 'Empty input', focused: true}, + {role: 'textbox', name: 'readonly input', readonly: true}, + {role: 'textbox', name: 'disabled input', disabled: true}, + {role: 'textbox', name: 'Input with whitespace', value: ' '}, + {role: 'textbox', name: '', value: 'value only'}, + {role: 'textbox', name: 'placeholder', value: 'and a value'}, + { + role: 'textbox', + name: 'placeholder', + value: 'and a value', + description: 'This is a description!', + }, + { + role: 'combobox', + name: '', + value: 'First Option', + haspopup: 'menu', + children: [ + {role: 'menuitem', name: 'First Option', selected: true}, + {role: 'menuitem', name: 'Second Option'}, + ], + }, + ], + }; + expect(await page.accessibility.snapshot()).toMatchObject(golden); + }); + it('should report uninteresting nodes', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(`<textarea>hi</textarea>`); + await page.focus('textarea'); + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'text leaf', + name: 'hi', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'hi', + focused: true, + multiline: true, + children: [ + { + role: 'generic', + name: '', + children: [ + { + role: 'StaticText', + name: 'hi', + }, + ], + }, + ], + }; + expect( + findFocusedNode( + await page.accessibility.snapshot({interestingOnly: false}) + ) + ).toMatchObject(golden); + }); + it('get snapshots while the tree is re-calculated', async () => { + // see https://github.com/puppeteer/puppeteer/issues/9404 + const {page} = await getTestState(); + + await page.setContent( + `<!DOCTYPE html> + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Accessible name + aria-expanded puppeteer bug</title> + <style> + [aria-expanded="false"] + * { + display: none; + } + </style> + </head> + <body> + <button hidden>Show</button> + <p>Some content</p> + <script> + const button = document.querySelector('button'); + button.removeAttribute('hidden') + button.setAttribute('aria-expanded', 'false'); + button.addEventListener('click', function() { + button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') !== 'true') + if (button.getAttribute('aria-expanded') == 'true') { + button.textContent = 'Hide' + } else { + button.textContent = 'Show' + } + }) + </script> + </body> + </html>` + ); + async function getAccessibleName(page: any, element: any) { + return (await page.accessibility.snapshot({root: element})).name; + } + using button = await page.$('button'); + expect(await getAccessibleName(page, button)).toEqual('Show'); + await button?.click(); + await page.waitForSelector('aria/Hide'); + }); + it('roledescription', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div tabIndex=-1 aria-roledescription="foo">Hi</div>' + ); + const snapshot = await page.accessibility.snapshot(); + // See https://chromium-review.googlesource.com/c/chromium/src/+/3088862 + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.roledescription).toBeUndefined(); + }); + it('orientation', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<a href="" role="slider" aria-orientation="vertical">11</a>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.orientation).toEqual('vertical'); + }); + it('autocomplete', async () => { + const {page} = await getTestState(); + + await page.setContent('<input type="number" aria-autocomplete="list" />'); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.autocomplete).toEqual('list'); + }); + it('multiselectable', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-multiselectable=true>hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.multiselectable).toEqual(true); + }); + it('keyshortcuts', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div role="grid" tabIndex=-1 aria-keyshortcuts="foo">hey</div>' + ); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + assert(snapshot.children[0]); + expect(snapshot.children[0]!.keyshortcuts).toEqual('foo'); + }); + describe('filtering children of leaf nodes', function () { + it('should not report text nodes inside controls', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div role="tablist"> + <div role="tab" aria-selected="true"><b>Tab1</b></div> + <div role="tab">Tab2</div> + </div>`); + const golden = isFirefox + ? { + role: 'document', + name: '', + children: [ + { + role: 'pagetab', + name: 'Tab1', + selected: true, + }, + { + role: 'pagetab', + name: 'Tab2', + }, + ], + } + : { + role: 'RootWebArea', + name: '', + children: [ + { + role: 'tab', + name: 'Tab1', + selected: true, + }, + { + role: 'tab', + name: 'Tab2', + }, + ], + }; + expect(await page.accessibility.snapshot()).toEqual(golden); + }); + it('rich text editable fields should have children', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div contenteditable="true"> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + const golden = isFirefox + ? { + role: 'section', + name: '', + children: [ + { + role: 'text leaf', + name: 'Edit this image:', + }, + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'generic', + name: '', + value: 'Edit this image: ', + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + { + role: 'image', + name: 'my fake image', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toMatchObject(golden); + }); + it('rich text editable fields with role should have children', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div contenteditable="true" role='textbox'> + Edit this image: <img src="fakeimage.png" alt="my fake image"> + </div>`); + // Image node should not be exposed in contenteditable elements. See https://crbug.com/1324392. + const golden = isFirefox + ? { + role: 'entry', + name: '', + value: 'Edit this image: my fake image', + children: [ + { + role: 'StaticText', + name: 'my fake image', + }, + ], + } + : { + role: 'textbox', + name: '', + value: 'Edit this image: ', + multiline: true, + children: [ + { + role: 'StaticText', + name: 'Edit this image: ', + }, + ], + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toMatchObject(golden); + }); + + // Firefox does not support contenteditable="plaintext-only". + describe('plaintext contenteditable', function () { + it('plain text field with role should not have children', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <div contenteditable="plaintext-only" role='textbox'>Edit this image:<img src="fakeimage.png" alt="my fake image"></div>`); + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual({ + role: 'textbox', + name: '', + value: 'Edit this image:', + multiline: true, + }); + }); + }); + it('non editable textbox with role and tabIndex and label should not have children', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div role="textbox" tabIndex=0 aria-checked="true" aria-label="my favorite textbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'entry', + name: 'my favorite textbox', + value: 'this is the inner content yo', + } + : { + role: 'textbox', + name: 'my favorite textbox', + value: 'this is the inner content ', + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox with and tabIndex and label should not have children', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div role="checkbox" tabIndex=0 aria-checked="true" aria-label="my favorite checkbox"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'my favorite checkbox', + checked: true, + } + : { + role: 'checkbox', + name: 'my favorite checkbox', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + it('checkbox without label should not have children', async () => { + const {page, isFirefox} = await getTestState(); + + await page.setContent(` + <div role="checkbox" aria-checked="true"> + this is the inner content + <img alt="yo" src="fakeimg.png"> + </div>`); + const golden = isFirefox + ? { + role: 'checkbutton', + name: 'this is the inner content yo', + checked: true, + } + : { + role: 'checkbox', + name: 'this is the inner content yo', + checked: true, + }; + const snapshot = await page.accessibility.snapshot(); + assert(snapshot); + assert(snapshot.children); + expect(snapshot.children[0]).toEqual(golden); + }); + + describe('root option', function () { + it('should work a button', async () => { + const {page} = await getTestState(); + + await page.setContent(`<button>My Button</button>`); + + using button = (await page.$('button'))!; + expect(await page.accessibility.snapshot({root: button})).toEqual({ + role: 'button', + name: 'My Button', + }); + }); + it('should work an input', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input title="My Input" value="My Value">`); + + using input = (await page.$('input'))!; + expect(await page.accessibility.snapshot({root: input})).toEqual({ + role: 'textbox', + name: 'My Input', + value: 'My Value', + }); + }); + it('should work a menu', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <div role="menu" title="My Menu"> + <div role="menuitem">First Item</div> + <div role="menuitem">Second Item</div> + <div role="menuitem">Third Item</div> + </div> + `); + + using menu = (await page.$('div[role="menu"]'))!; + expect(await page.accessibility.snapshot({root: menu})).toEqual({ + role: 'menu', + name: 'My Menu', + children: [ + {role: 'menuitem', name: 'First Item'}, + {role: 'menuitem', name: 'Second Item'}, + {role: 'menuitem', name: 'Third Item'}, + ], + orientation: 'vertical', + }); + }); + it('should return null when the element is no longer in DOM', async () => { + const {page} = await getTestState(); + + await page.setContent(`<button>My Button</button>`); + using button = (await page.$('button'))!; + await page.$eval('button', button => { + return button.remove(); + }); + expect(await page.accessibility.snapshot({root: button})).toEqual(null); + }); + it('should support the interestingOnly option', async () => { + const {page} = await getTestState(); + + await page.setContent(`<div><button>My Button</button></div>`); + using div = (await page.$('div'))!; + expect(await page.accessibility.snapshot({root: div})).toEqual(null); + expect( + await page.accessibility.snapshot({ + root: div, + interestingOnly: false, + }) + ).toMatchObject({ + role: 'generic', + name: '', + children: [ + { + role: 'button', + name: 'My Button', + children: [{role: 'StaticText', name: 'My Button'}], + }, + ], + }); + }); + }); + }); + + function findFocusedNode( + node: SerializedAXNode | null + ): SerializedAXNode | null { + if (node?.focused) { + return node; + } + for (const child of node?.children || []) { + const focusedChild = findFocusedNode(child); + if (focusedChild) { + return focusedChild; + } + } + return null; + } +}); diff --git a/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts new file mode 100644 index 0000000000..434d01426a --- /dev/null +++ b/remote/test/puppeteer/test/src/ariaqueryhandler.spec.ts @@ -0,0 +1,721 @@ +/** + * @license + * Copyright 2020 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame, detachFrame} from './utils.js'; + +describe('AriaQueryHandler', () => { + setupTestBrowserHooks(); + + describe('parseAriaSelector', () => { + it('should find button', async () => { + const {page} = await getTestState(); + await page.setContent( + '<button id="btn" role="button"> Submit button and some spaces </button>' + ); + const expectFound = async (button: ElementHandle | null) => { + assert(button); + const id = await button.evaluate((button: Element) => { + return button.id; + }); + expect(id).toBe('btn'); + }; + { + using button = await page.$( + 'aria/Submit button and some spaces[role="button"]' + ); + await expectFound(button); + } + { + using button = await page.$( + "aria/Submit button and some spaces[role='button']" + ); + await expectFound(button); + } + using button = await page.$( + 'aria/ Submit button and some spaces[role="button"]' + ); + await expectFound(button); + { + using button = await page.$( + 'aria/Submit button and some spaces [role="button"]' + ); + await expectFound(button); + } + { + using button = await page.$( + 'aria/Submit button and some spaces [ role = "button" ] ' + ); + await expectFound(button); + } + { + using button = await page.$( + 'aria/[role="button"]Submit button and some spaces' + ); + await expectFound(button); + } + { + using button = await page.$( + 'aria/Submit button [role="button"]and some spaces' + ); + await expectFound(button); + } + { + using button = await page.$( + 'aria/[name=" Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + } + { + using button = await page.$( + "aria/[name=' Submit button and some spaces'][role='button']" + ); + await expectFound(button); + } + { + using button = await page.$( + 'aria/ignored[name="Submit button and some spaces"][role="button"]' + ); + await expectFound(button); + await expect(page.$('aria/smth[smth="true"]')).rejects.toThrow( + 'Unknown aria attribute "smth" in selector' + ); + } + }); + }); + + describe('queryOne', () => { + it('should find button by role', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + using button = (await page.$( + 'aria/[role="button"]' + )) as ElementHandle<HTMLButtonElement>; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find button by name and role', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="div"><button id="btn" role="button">Submit</button></div>' + ); + using button = (await page.$( + 'aria/Submit[role="button"]' + )) as ElementHandle<HTMLButtonElement>; + const id = await button!.evaluate(button => { + return button.id; + }); + expect(id).toBe('btn'); + }); + + it('should find first matching element', async () => { + const {page} = await getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + using div = (await page.$( + 'aria/menu div' + )) as ElementHandle<HTMLDivElement>; + const id = await div!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find by name', async () => { + const {page} = await getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + using menu = (await page.$( + 'aria/menu-label1' + )) as ElementHandle<HTMLDivElement>; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu1'); + }); + + it('should find 2nd element by name', async () => { + const {page} = await getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu-label1">menu div</div> + <div role="menu" id="mnu2" aria-label="menu-label2">menu div</div> + ` + ); + using menu = (await page.$( + 'aria/menu-label2' + )) as ElementHandle<HTMLDivElement>; + const id = await menu!.evaluate(div => { + return div.id; + }); + expect(id).toBe('mnu2'); + }); + }); + + describe('queryAll', () => { + it('should find menu by name', async () => { + const {page} = await getTestState(); + await page.setContent( + ` + <div role="menu" id="mnu1" aria-label="menu div"></div> + <div role="menu" id="mnu2" aria-label="menu div"></div> + ` + ); + const divs = (await page.$$('aria/menu div')) as Array< + ElementHandle<HTMLDivElement> + >; + const ids = await Promise.all( + divs.map(n => { + return n.evaluate(div => { + return div.id; + }); + }) + ); + expect(ids.join(', ')).toBe('mnu1, mnu2'); + }); + }); + describe('queryAllArray', () => { + it('$$eval should handle many elements', async function () { + this.timeout(40_000); + + const {page} = await getTestState(); + await page.setContent(''); + await page.evaluate( + ` + for (var i = 0; i <= 10000; i++) { + const button = document.createElement('button'); + button.textContent = i; + document.body.appendChild(button); + } + ` + ); + const sum = await page.$$eval('aria/[role="button"]', buttons => { + return buttons.reduce((acc, button) => { + return acc + Number(button.textContent); + }, 0); + }); + expect(sum).toBe(50005000); + }); + }); + + describe('waitForSelector (aria)', 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); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should work for ElementHandle.waitForSelector', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (document.body.innerHTML = `<div><button>test</button></div>`); + }); + using element = (await page.$('div'))!; + await element!.waitForSelector('aria/test'); + }); + + it('should persist query handler bindings across reloads', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + await page.reload(); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should persist query handler bindings across navigations', async () => { + const {page, server} = await getTestState(); + + // Reset page but make sure that execution context ids start with 1. + await page.goto('data:text/html,'); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + + // Reset page but again make sure that execution context ids start with 1. + await page.goto('data:text/html,'); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + }); + + it('should work independently of `exposeFunction`', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('ariaQuerySelector', (a: number, b: number) => { + return a + b; + }); + await page.evaluate(addElement, 'button'); + await page.waitForSelector('aria/[role="button"]'); + const result = await page.evaluate('globalThis.ariaQuerySelector(2,8)'); + expect(result).toBe(10); + }); + + it('should work with removed MutationObserver', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + // @ts-expect-error This is the point of the test. + return delete window.MutationObserver; + }); + const [handle] = await Promise.all([ + page.waitForSelector('aria/anything'), + page.setContent(`<h1>anything</h1>`), + ]); + assert(handle); + 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('aria/[role="heading"]'); + await frame.evaluate(addElement, 'br'); + await frame.evaluate(addElement, 'h1'); + using elementHandle = (await watchdog)!; + const tagName = await ( + await elementHandle.getProperty('tagName') + ).jsonValue(); + expect(tagName).toBe('H1'); + }); + + 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('aria/name'); + await page.evaluate(addElement, 'span'); + await page.evaluate(() => { + return (document.querySelector('span')!.innerHTML = + '<h3><div aria-label="name"></div></h3>'); + }); + 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('aria/[role="button"]'); + await otherFrame!.evaluate(addElement, 'button'); + await page.evaluate(addElement, 'button'); + using elementHandle = await watchdog; + expect(elementHandle!.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( + 'aria/[role="button"]' + ); + await frame1!.evaluate(addElement, 'button'); + await frame2!.evaluate(addElement, 'button'); + using elementHandle = await waitForSelectorPromise; + expect(elementHandle!.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; + const waitPromise = frame! + .waitForSelector('aria/does-not-exist') + .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} = await getTestState(); + + let imgFound = false; + const waitForSelector = page + .waitForSelector('aria/[role="image"]') + .then(() => { + return (imgFound = true); + }); + await page.goto(server.EMPTY_PAGE); + expect(imgFound).toBe(false); + await page.reload(); + expect(imgFound).toBe(false); + await page.goto(server.CROSS_PROCESS_PREFIX + '/grid.html'); + await waitForSelector; + expect(imgFound).toBe(true); + }); + + it('should wait for visible', async () => { + const {page} = await getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/name', {visible: true}) + .then(() => { + return (divFound = true); + }); + await page.setContent( + `<div aria-label='name' style='display: none; visibility: hidden;'>1</div>` + ); + expect(divFound).toBe(false); + await page.evaluate(() => { + return document.querySelector('div')!.style.removeProperty('display'); + }); + expect(divFound).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div')! + .style.removeProperty('visibility'); + }); + expect(await waitForSelector).toBe(true); + expect(divFound).toBe(true); + }); + + it('should wait for visible recursively', async () => { + const {page} = await getTestState(); + + let divVisible = false; + const waitForSelector = page + .waitForSelector('aria/inner', {visible: true}) + .then(() => { + return (divVisible = true); + }) + .catch(() => { + return (divVisible = false); + }); + await page.setContent( + `<div style='display: none; visibility: hidden;'><div aria-label="inner">hi</div></div>` + ); + expect(divVisible).toBe(false); + await page.evaluate(() => { + return document.querySelector('div')!.style.removeProperty('display'); + }); + expect(divVisible).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div')! + .style.removeProperty('visibility'); + }); + expect(await waitForSelector).toBe(true); + expect(divVisible).toBe(true); + }); + + it('hidden should wait for visibility: hidden', async () => { + const {page} = await getTestState(); + + let divHidden = false; + await page.setContent( + `<div role='button' style='display: block;'>text</div>` + ); + const waitForSelector = page + .waitForSelector('aria/[role="button"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }) + .catch(() => { + return (divHidden = false); + }); + await page.waitForSelector('aria/[role="button"]'); // do a round trip + expect(divHidden).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div')! + .style.setProperty('visibility', 'hidden'); + }); + expect(await waitForSelector).toBe(true); + expect(divHidden).toBe(true); + }); + + it('hidden should wait for display: none', async () => { + const {page} = await getTestState(); + + let divHidden = false; + await page.setContent( + `<div role='main' style='display: block;'>text</div>` + ); + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divHidden = true); + }) + .catch(() => { + return (divHidden = false); + }); + await page.waitForSelector('aria/[role="main"]'); // 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 wait for removal', async () => { + const {page} = await getTestState(); + + await page.setContent(`<div role='main'>text</div>`); + let divRemoved = false; + const waitForSelector = page + .waitForSelector('aria/[role="main"]', {hidden: true}) + .then(() => { + return (divRemoved = true); + }) + .catch(() => { + return (divRemoved = false); + }); + await page.waitForSelector('aria/[role="main"]'); // do a round trip + expect(divRemoved).toBe(false); + await page.evaluate(() => { + return document.querySelector('div')!.remove(); + }); + expect(await waitForSelector).toBe(true); + expect(divRemoved).toBe(true); + }); + + it('should return null if waiting to hide non-existing element', async () => { + const {page} = await getTestState(); + + using handle = await page.waitForSelector('aria/non-existing', { + hidden: true, + }); + expect(handle).toBe(null); + }); + + it('should respect timeout', async () => { + const {page} = await getTestState(); + + const error = await page + .waitForSelector('aria/[role="button"]', { + timeout: 10, + }) + .catch(error => { + return error; + }); + expect(error.message).toContain( + 'Waiting for selector `[role="button"]` failed: Waiting failed: 10ms exceeded' + ); + expect(error).toBeInstanceOf(TimeoutError); + }); + + it('should have an error message specifically for awaiting an element to be hidden', async () => { + const {page} = await getTestState(); + + await page.setContent(`<div role='main'>text</div>`); + const promise = page.waitForSelector('aria/[role="main"]', { + hidden: true, + timeout: 10, + }); + await expect(promise).rejects.toMatchObject({ + message: + 'Waiting for selector `[role="main"]` failed: Waiting failed: 10ms exceeded', + }); + }); + + it('should respond to node attribute mutation', async () => { + const {page} = await getTestState(); + + let divFound = false; + const waitForSelector = page + .waitForSelector('aria/zombo') + .then(() => { + return (divFound = true); + }) + .catch(() => { + return (divFound = false); + }); + await page.setContent(`<div aria-label='notZombo'></div>`); + expect(divFound).toBe(false); + await page.evaluate(() => { + return document + .querySelector('div')! + .setAttribute('aria-label', 'zombo'); + }); + expect(await waitForSelector).toBe(true); + }); + + it('should return the element handle', async () => { + const {page} = await getTestState(); + + const waitForSelector = page.waitForSelector('aria/zombo').catch(err => { + return err; + }); + await page.setContent(`<div aria-label='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('aria/zombo', {timeout: 10}).catch(error_ => { + return (error = error_); + }); + expect(error!.stack).toContain( + 'Waiting for selector `zombo` failed: Waiting failed: 10ms exceeded' + ); + }); + }); + + describe('queryOne (Chromium web test)', () => { + async function setupPage(): ReturnType<typeof getTestState> { + const state = await getTestState(); + await state.page.setContent( + ` + <h2 id="shown">title</h2> + <h2 id="hidden" aria-hidden="true">title</h2> + <div id="node1" aria-labeledby="node2"></div> + <div id="node2" aria-label="bar"></div> + <div id="node3" aria-label="foo"></div> + <div id="node4" class="container"> + <div id="node5" role="button" aria-label="foo"></div> + <div id="node6" role="button" aria-label="foo"></div> + <!-- Accessible name not available when element is hidden --> + <div id="node7" hidden role="button" aria-label="foo"></div> + <div id="node8" role="button" aria-label="bar"></div> + </div> + <button id="node10">text content</button> + <h1 id="node11">text content</h1> + <!-- Accessible name not available when role is "presentation" --> + <h1 id="node12" role="presentation">text content</h1> + <!-- Elements inside shadow dom should be found --> + <script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const h1 = document.createElement('h1'); + h1.textContent = 'text content'; + h1.id = 'node13'; + shadowRoot.appendChild(h1); + document.documentElement.appendChild(div); + </script> + <img id="node20" src="" alt="Accessible Name"> + <input id="node21" type="submit" value="Accessible Name"> + <label id="node22" for="node23">Accessible Name</label> + <!-- Accessible name for the <input> is "Accessible Name" --> + <input id="node23"> + <div id="node24" title="Accessible Name"></div> + <div role="treeitem" id="node30"> + <div role="treeitem" id="node31"> + <div role="treeitem" id="node32">item1</div> + <div role="treeitem" id="node33">item2</div> + </div> + <div role="treeitem" id="node34">item3</div> + </div> + <!-- Accessible name for the <div> is "item1 item2 item3" --> + <div aria-describedby="node30"></div> + ` + ); + return state; + } + const getIds = async (elements: ElementHandle[]) => { + return await Promise.all( + elements.map(element => { + return element.evaluate((element: Element) => { + return element.id; + }); + }) + ); + }; + it('should find by name "foo"', async () => { + const {page} = await setupPage(); + const found = await page.$$('aria/foo'); + const ids = await getIds(found); + expect(ids).toEqual(['node3', 'node5', 'node6']); + }); + it('should find by name "bar"', async () => { + const {page} = await setupPage(); + const found = await page.$$('aria/bar'); + const ids = await getIds(found); + expect(ids).toEqual(['node1', 'node2', 'node8']); + }); + it('should find treeitem by name', async () => { + const {page} = await setupPage(); + const found = await page.$$('aria/item1 item2 item3'); + const ids = await getIds(found); + expect(ids).toEqual(['node30']); + }); + it('should find by role "button"', async () => { + const {page} = await setupPage(); + const found = (await page.$$('aria/[role="button"]')) as Array< + ElementHandle<HTMLButtonElement> + >; + const ids = await getIds(found); + expect(ids).toEqual([ + 'node5', + 'node6', + 'node7', + 'node8', + 'node10', + 'node21', + ]); + }); + it('should find by role "heading"', async () => { + const {page} = await setupPage(); + const found = await page.$$('aria/[role="heading"]'); + const ids = await getIds(found); + expect(ids).toEqual(['shown', 'hidden', 'node11', 'node13']); + }); + it('should find both ignored and unignored', async () => { + const {page} = await setupPage(); + const found = await page.$$('aria/title'); + const ids = await getIds(found); + expect(ids).toEqual(['shown']); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/autofill.spec.ts b/remote/test/puppeteer/test/src/autofill.spec.ts new file mode 100644 index 0000000000..a04e4b8e8b --- /dev/null +++ b/remote/test/puppeteer/test/src/autofill.spec.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Autofill', function () { + setupTestBrowserHooks(); + describe('ElementHandle.autofill', () => { + it('should fill out a credit card', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/credit-card.html'); + using name = await page.waitForSelector('#name'); + await name!.autofill({ + creditCard: { + number: '4444444444444444', + name: 'John Smith', + expiryMonth: '01', + expiryYear: '2030', + cvc: '123', + }, + }); + expect( + await page.evaluate(() => { + const result = []; + for (const el of document.querySelectorAll('input')) { + result.push(el.value); + } + return result.join(','); + }) + ).toBe('John Smith,4444444444444444,01,2030,Submit'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/browser.spec.ts b/remote/test/puppeteer/test/src/browser.spec.ts new file mode 100644 index 0000000000..6f21af5d9a --- /dev/null +++ b/remote/test/puppeteer/test/src/browser.spec.ts @@ -0,0 +1,81 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Browser specs', function () { + setupTestBrowserHooks(); + + describe('Browser.version', function () { + it('should return version', async () => { + const {browser} = await getTestState(); + + const version = await browser.version(); + expect(version.length).toBeGreaterThan(0); + expect(version.toLowerCase()).atLeastOneToContain(['firefox', 'chrome']); + }); + }); + + describe('Browser.userAgent', function () { + it('should include Browser engine', async () => { + const {browser, isChrome} = await getTestState(); + + const userAgent = await browser.userAgent(); + expect(userAgent.length).toBeGreaterThan(0); + if (isChrome) { + expect(userAgent).toContain('WebKit'); + } else { + expect(userAgent).toContain('Gecko'); + } + }); + }); + + describe('Browser.target', function () { + it('should return browser target', async () => { + const {browser} = await getTestState(); + + const target = browser.target(); + expect(target.type()).toBe('browser'); + }); + }); + + describe('Browser.process', function () { + it('should return child_process instance', async () => { + const {browser} = await getTestState(); + + const process = await browser.process(); + expect(process!.pid).toBeGreaterThan(0); + }); + it('should not return child_process for remote browser', async () => { + const {browser, puppeteer} = await getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + protocol: browser.protocol, + }); + expect(remoteBrowser.process()).toBe(null); + await remoteBrowser.disconnect(); + }); + }); + + describe('Browser.isConnected', () => { + it('should set the browser connected state', async () => { + const {browser, puppeteer} = await getTestState(); + + const browserWSEndpoint = browser.wsEndpoint(); + const newBrowser = await puppeteer.connect({ + browserWSEndpoint, + protocol: browser.protocol, + }); + expect(newBrowser.isConnected()).toBe(true); + await newBrowser.disconnect(); + expect(newBrowser.isConnected()).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/browsercontext.spec.ts b/remote/test/puppeteer/test/src/browsercontext.spec.ts new file mode 100644 index 0000000000..9cbbda60a4 --- /dev/null +++ b/remote/test/puppeteer/test/src/browsercontext.spec.ts @@ -0,0 +1,368 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('BrowserContext', function () { + setupTestBrowserHooks(); + + it('should have default context', async () => { + const {browser} = await getTestState({ + skipContextCreation: true, + }); + expect(browser.browserContexts()).toHaveLength(1); + const defaultContext = browser.browserContexts()[0]!; + expect(defaultContext!.isIncognito()).toBe(false); + let error!: Error; + await defaultContext!.close().catch(error_ => { + return (error = error_); + }); + expect(browser.defaultBrowserContext()).toBe(defaultContext); + expect(error.message).toContain('cannot be closed'); + }); + it('should create new incognito context', async () => { + const {browser} = await getTestState({ + skipContextCreation: true, + }); + + expect(browser.browserContexts()).toHaveLength(1); + const context = await browser.createIncognitoBrowserContext(); + expect(context.isIncognito()).toBe(true); + expect(browser.browserContexts()).toHaveLength(2); + expect(browser.browserContexts().indexOf(context) !== -1).toBe(true); + await context.close(); + expect(browser.browserContexts()).toHaveLength(1); + }); + it('should close all belonging targets once closing context', async () => { + const {browser} = await getTestState({ + skipContextCreation: true, + }); + + expect(await browser.pages()).toHaveLength(1); + + const context = await browser.createIncognitoBrowserContext(); + await context.newPage(); + expect(await browser.pages()).toHaveLength(2); + expect(await context.pages()).toHaveLength(1); + + await context.close(); + expect(await browser.pages()).toHaveLength(1); + }); + it('window.open should use parent tab context', async () => { + const {browser, server, page, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [popupTarget] = await Promise.all([ + waitEvent(browser, 'targetcreated'), + page.evaluate(url => { + return window.open(url); + }, server.EMPTY_PAGE), + ]); + expect(popupTarget.browserContext()).toBe(context); + }); + it('should fire target events', async () => { + const {server, context} = await getTestState(); + + const events: string[] = []; + context.on('targetcreated', target => { + events.push('CREATED: ' + target.url()); + }); + context.on('targetchanged', target => { + events.push('CHANGED: ' + target.url()); + }); + context.on('targetdestroyed', target => { + events.push('DESTROYED: ' + target.url()); + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual([ + 'CREATED: about:blank', + `CHANGED: ${server.EMPTY_PAGE}`, + `DESTROYED: ${server.EMPTY_PAGE}`, + ]); + }); + it('should wait for a target', async () => { + const {server, context} = await getTestState(); + + let resolved = false; + + const targetPromise = context.waitForTarget(target => { + return target.url() === server.EMPTY_PAGE; + }); + targetPromise + .then(() => { + return (resolved = true); + }) + .catch(error => { + resolved = true; + if (error instanceof TimeoutError) { + console.error(error); + } else { + throw error; + } + }); + const page = await context.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof TimeoutError) { + console.error(error); + } else { + throw error; + } + } + }); + + it('should timeout waiting for a non-existent target', async () => { + const {browser, server} = await getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + const error = await context + .waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return error_; + }); + expect(error).toBeInstanceOf(TimeoutError); + await context.close(); + }); + + it('should isolate localStorage and cookies', async () => { + const {browser, server} = await getTestState({ + skipContextCreation: true, + }); + + // Create two incognito contexts. + const context1 = await browser.createIncognitoBrowserContext(); + const context2 = await browser.createIncognitoBrowserContext(); + expect(context1.targets()).toHaveLength(0); + expect(context2.targets()).toHaveLength(0); + + // Create a page in first incognito context. + const page1 = await context1.newPage(); + await page1.goto(server.EMPTY_PAGE); + await page1.evaluate(() => { + localStorage.setItem('name', 'page1'); + document.cookie = 'name=page1'; + }); + + expect(context1.targets()).toHaveLength(1); + expect(context2.targets()).toHaveLength(0); + + // Create a page in second incognito context. + const page2 = await context2.newPage(); + await page2.goto(server.EMPTY_PAGE); + await page2.evaluate(() => { + localStorage.setItem('name', 'page2'); + document.cookie = 'name=page2'; + }); + + expect(context1.targets()).toHaveLength(1); + expect(context1.targets()[0]).toBe(page1.target()); + expect(context2.targets()).toHaveLength(1); + expect(context2.targets()[0]).toBe(page2.target()); + + // Make sure pages don't share localstorage or cookies. + expect( + await page1.evaluate(() => { + return localStorage.getItem('name'); + }) + ).toBe('page1'); + expect( + await page1.evaluate(() => { + return document.cookie; + }) + ).toBe('name=page1'); + expect( + await page2.evaluate(() => { + return localStorage.getItem('name'); + }) + ).toBe('page2'); + expect( + await page2.evaluate(() => { + return document.cookie; + }) + ).toBe('name=page2'); + + // Cleanup contexts. + await Promise.all([context1.close(), context2.close()]); + expect(browser.browserContexts()).toHaveLength(1); + }); + + it('should work across sessions', async () => { + const {browser, puppeteer} = await getTestState({ + skipContextCreation: true, + }); + + expect(browser.browserContexts()).toHaveLength(1); + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts()).toHaveLength(2); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + const contexts = remoteBrowser.browserContexts(); + expect(contexts).toHaveLength(2); + await remoteBrowser.disconnect(); + await context.close(); + }); + + it('should provide a context id', async () => { + const {browser} = await getTestState({ + skipContextCreation: true, + }); + + expect(browser.browserContexts()).toHaveLength(1); + expect(browser.browserContexts()[0]!.id).toBeUndefined(); + + const context = await browser.createIncognitoBrowserContext(); + expect(browser.browserContexts()).toHaveLength(2); + expect(browser.browserContexts()[1]!.id).toBeDefined(); + await context.close(); + }); + + describe('BrowserContext.overridePermissions', function () { + function getPermission(page: Page, name: PermissionName) { + return page.evaluate(name => { + return navigator.permissions.query({name}).then(result => { + return result.state; + }); + }, name); + } + + it('should be prompt by default', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it('should deny permission when not listed', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + }); + it('should fail when bad permission is given', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error!: Error; + await context + // @ts-expect-error purposeful bad input for test + .overridePermissions(server.EMPTY_PAGE, ['foo']) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unknown permission: foo'); + }); + it('should grant permission when listed', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + }); + it('should reset permissions', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect(await getPermission(page, 'geolocation')).toBe('granted'); + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + }); + it('should trigger permission onchange', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + (globalThis as any).events = []; + return navigator.permissions + .query({name: 'geolocation'}) + .then(function (result) { + (globalThis as any).events.push(result.state); + result.onchange = function () { + (globalThis as any).events.push(result.state); + }; + }); + }); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt']); + await context.overridePermissions(server.EMPTY_PAGE, []); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied']); + await context.overridePermissions(server.EMPTY_PAGE, ['geolocation']); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied', 'granted']); + await context.clearPermissionOverrides(); + expect( + await page.evaluate(() => { + return (globalThis as any).events; + }) + ).toEqual(['prompt', 'denied', 'granted', 'prompt']); + }); + it('should isolate permissions between browser contexts', async () => { + const {page, server, context, browser} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const otherContext = await browser.createIncognitoBrowserContext(); + const otherPage = await otherContext.newPage(); + await otherPage.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('prompt'); + + await context.overridePermissions(server.EMPTY_PAGE, []); + await otherContext.overridePermissions(server.EMPTY_PAGE, [ + 'geolocation', + ]); + expect(await getPermission(page, 'geolocation')).toBe('denied'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await context.clearPermissionOverrides(); + expect(await getPermission(page, 'geolocation')).toBe('prompt'); + expect(await getPermission(otherPage, 'geolocation')).toBe('granted'); + + await otherContext.close(); + }); + it('should grant persistent-storage', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(await getPermission(page, 'persistent-storage')).not.toBe( + 'granted' + ); + await context.overridePermissions(server.EMPTY_PAGE, [ + 'persistent-storage', + ]); + expect(await getPermission(page, 'persistent-storage')).toBe('granted'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts new file mode 100644 index 0000000000..2000c0e435 --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/CDPSession.spec.ts @@ -0,0 +1,147 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {Target} from 'puppeteer-core/internal/api/Target.js'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; + +import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js'; +import {waitEvent} from '../utils.js'; + +describe('Target.createCDPSession', function () { + setupTestBrowserHooks(); + + it('should work', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + + await Promise.all([ + client.send('Runtime.enable'), + client.send('Runtime.evaluate', {expression: 'window.foo = "bar"'}), + ]); + const foo = await page.evaluate(() => { + return (globalThis as any).foo; + }); + expect(foo).toBe('bar'); + }); + + it('should not report created targets for custom CDP sessions', async () => { + const {browser} = await getTestState(); + let called = 0; + const handler = async (target: Target) => { + called++; + if (called > 1) { + throw new Error('Too many targets created'); + } + await target.createCDPSession(); + }; + browser.browserContexts()[0]!.on('targetcreated', handler); + await browser.newPage(); + browser.browserContexts()[0]!.off('targetcreated', handler); + }); + + it('should send events', async () => { + const {page, server} = await getTestState(); + + const client = await page.createCDPSession(); + await client.send('Network.enable'); + const events: unknown[] = []; + client.on('Network.requestWillBeSent', event => { + events.push(event); + }); + await Promise.all([ + waitEvent(client, 'Network.requestWillBeSent'), + page.goto(server.EMPTY_PAGE), + ]); + expect(events).toHaveLength(1); + }); + it('should enable and disable domains independently', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + await client.send('Runtime.enable'); + await client.send('Debugger.enable'); + // JS coverage enables and then disables Debugger domain. + await page.coverage.startJSCoverage(); + await page.coverage.stopJSCoverage(); + // generate a script in page and wait for the event. + const [event] = await Promise.all([ + waitEvent(client, 'Debugger.scriptParsed'), + page.evaluate('//# sourceURL=foo.js'), + ]); + // expect events to be dispatched. + expect(event.url).toBe('foo.js'); + }); + it('should be able to detach session', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + await client.send('Runtime.enable'); + const evalResponse = await client.send('Runtime.evaluate', { + expression: '1 + 2', + returnByValue: true, + }); + expect(evalResponse.result.value).toBe(3); + await client.detach(); + let error!: Error; + try { + await client.send('Runtime.evaluate', { + expression: '3 + 1', + returnByValue: true, + }); + } catch (error_) { + if (isErrorLike(error_)) { + error = error_ as Error; + } + } + expect(error.message).toContain('Session closed.'); + }); + it('should throw nice errors', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + const error = await theSourceOfTheProblems().catch(error => { + return error; + }); + expect(error.stack).toContain('theSourceOfTheProblems'); + expect(error.message).toContain('ThisCommand.DoesNotExist'); + + async function theSourceOfTheProblems() { + // @ts-expect-error This fails in TS as it knows that command does not + // exist but we want to have this tests for our users who consume in JS + // not TS. + await client.send('ThisCommand.DoesNotExist'); + } + }); + + it('should respect custom timeout', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + await expect( + client.send( + 'Runtime.evaluate', + { + expression: 'new Promise(resolve => {})', + awaitPromise: true, + }, + { + timeout: 50, + } + ) + ).rejects.toThrowError( + `Runtime.evaluate timed out. Increase the 'protocolTimeout' setting in launch/connect calls for a higher timeout if needed.` + ); + }); + + it('should expose the underlying connection', async () => { + const {page} = await getTestState(); + + const client = await page.createCDPSession(); + expect(client.connection()).toBeTruthy(); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts new file mode 100644 index 0000000000..d1f8992530 --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/TargetManager.spec.ts @@ -0,0 +1,96 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {CdpBrowser} from 'puppeteer-core/internal/cdp/Browser.js'; + +import {getTestState, launch} from '../mocha-utils.js'; +import {attachFrame} from '../utils.js'; + +describe('TargetManager', () => { + /* We use a special browser for this test as we need the --site-per-process flag */ + let state: Awaited<ReturnType<typeof launch>> & { + browser: CdpBrowser; + }; + + beforeEach(async () => { + const {defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + state = (await launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat([ + '--site-per-process', + '--remote-debugging-port=21222', + '--host-rules=MAP * 127.0.0.1', + ]), + }), + {createPage: false} + )) as Awaited<ReturnType<typeof launch>> & { + browser: CdpBrowser; + }; + }); + + afterEach(async () => { + await state.close(); + }); + + // CDP-specific test. + it('should handle targets', async () => { + const {server, context, browser} = state; + + const targetManager = browser._targetManager(); + expect(targetManager.getAvailableTargets().size).toBe(3); + + expect(await context.pages()).toHaveLength(0); + expect(targetManager.getAvailableTargets().size).toBe(3); + + const page = await context.newPage(); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(5); + + await page.goto(server.EMPTY_PAGE); + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(5); + + // attach a local iframe. + let framePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/empty.html'); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(5); + expect(page.frames()).toHaveLength(2); + + // // attach a remote frame iframe. + framePromise = page.waitForFrame(frame => { + return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + await attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(6); + expect(page.frames()).toHaveLength(3); + + framePromise = page.waitForFrame(frame => { + return frame.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }); + await attachFrame( + page, + 'frame3', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(await context.pages()).toHaveLength(1); + expect(targetManager.getAvailableTargets().size).toBe(7); + expect(page.frames()).toHaveLength(4); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts new file mode 100644 index 0000000000..211f93cd6b --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/bfcache.spec.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {PageEvent} from 'puppeteer-core'; + +import {launch} from '../mocha-utils.js'; +import {waitEvent} from '../utils.js'; + +describe('BFCache', function () { + it('can navigate to a BFCached page', async () => { + const {httpsServer, page, close} = await launch({ + ignoreHTTPSErrors: true, + }); + + try { + page.setDefaultTimeout(3000); + + await page.goto(httpsServer.PREFIX + '/cached/bfcache/index.html'); + + await Promise.all([page.waitForNavigation(), page.locator('a').click()]); + + expect(page.url()).toContain('target.html'); + + await Promise.all([page.waitForNavigation(), page.goBack()]); + + expect( + await page.evaluate(() => { + return document.body.innerText; + }) + ).toBe('BFCachednext'); + } finally { + await close(); + } + }); + + it('can navigate to a BFCached page containing an OOPIF and a worker', async () => { + const {httpsServer, page, close} = await launch({ + ignoreHTTPSErrors: true, + }); + try { + page.setDefaultTimeout(3000); + const [worker1] = await Promise.all([ + waitEvent(page, PageEvent.WorkerCreated), + page.goto( + httpsServer.PREFIX + '/cached/bfcache/worker-iframe-container.html' + ), + ]); + expect(await worker1.evaluate('1 + 1')).toBe(2); + await Promise.all([page.waitForNavigation(), page.locator('a').click()]); + + const [worker2] = await Promise.all([ + waitEvent(page, PageEvent.WorkerCreated), + page.waitForNavigation(), + page.goBack(), + ]); + expect(await worker2.evaluate('1 + 1')).toBe(2); + } finally { + await close(); + } + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/devtools.spec.ts b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts new file mode 100644 index 0000000000..c158481af2 --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/devtools.spec.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js'; + +import {getTestState, launch} from '../mocha-utils.js'; + +describe('DevTools', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20_000); + + let launchOptions: PuppeteerLaunchOptions & { + devtools: boolean; + }; + const browsers: Array<() => Promise<void>> = []; + + beforeEach(async () => { + const {defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + launchOptions = Object.assign({}, defaultBrowserOptions, { + devtools: true, + }); + }); + + async function launchBrowser(options: typeof launchOptions) { + const {browser, close} = await launch(options, {createContext: false}); + browsers.push(close); + return browser; + } + + afterEach(async () => { + await Promise.all( + browsers.map((close, index) => { + delete browsers[index]; + return close(); + }) + ); + }); + + it('target.page() should return a DevTools page if custom isPageTarget is provided', async function () { + const {puppeteer} = await getTestState({skipLaunch: true}); + const originalBrowser = await launchBrowser(launchOptions); + + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + _isPageTarget(target) { + return ( + target.type() === 'other' && target.url().startsWith('devtools://') + ); + }, + }); + const devtoolsPageTarget = await browser.waitForTarget(target => { + return target.type() === 'other'; + }); + const page = (await devtoolsPageTarget.page())!; + expect( + await page.evaluate(() => { + return 2 * 3; + }) + ).toBe(6); + expect(await browser.pages()).toContainEqual(page); + }); + it('target.page() should return a DevTools page if asPage is used', async function () { + const {puppeteer} = await getTestState({skipLaunch: true}); + const originalBrowser = await launchBrowser(launchOptions); + + const browserWSEndpoint = originalBrowser.wsEndpoint(); + + const browser = await puppeteer.connect({ + browserWSEndpoint, + }); + const devtoolsPageTarget = await browser.waitForTarget(target => { + return target.type() === 'other'; + }); + const page = (await devtoolsPageTarget.asPage())!; + expect( + await page.evaluate(() => { + return 2 * 3; + }) + ).toBe(6); + expect(await browser.pages()).toContainEqual(page); + }); + it('should open devtools when "devtools: true" option is given', async () => { + const browser = await launchBrowser( + Object.assign({devtools: true}, launchOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + await Promise.all([ + context.newPage(), + browser.waitForTarget((target: {url: () => string | string[]}) => { + return target.url().includes('devtools://'); + }), + ]); + await browser.close(); + }); + it('should expose DevTools as a page', async () => { + const browser = await launchBrowser( + Object.assign({devtools: true}, launchOptions) + ); + const context = await browser.createIncognitoBrowserContext(); + const [target] = await Promise.all([ + browser.waitForTarget((target: {url: () => string | string[]}) => { + return target.url().includes('devtools://'); + }), + context.newPage(), + ]); + const page = await target.page(); + await page!.waitForFunction(() => { + // @ts-expect-error wrong context. + return Boolean(DevToolsAPI); + }); + await browser.close(); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/extensions.spec.ts b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts new file mode 100644 index 0000000000..6db9f931ad --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/extensions.spec.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path'; + +import expect from 'expect'; +import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js'; + +import {getTestState, launch} from '../mocha-utils.js'; + +const extensionPath = path.join( + __dirname, + '..', + '..', + 'assets', + 'simple-extension' +); +const serviceWorkerExtensionPath = path.join( + __dirname, + '..', + '..', + 'assets', + 'serviceworkers', + 'extension' +); + +describe('extensions', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20_000); + + let extensionOptions: PuppeteerLaunchOptions & { + args: string[]; + }; + const browsers: Array<() => Promise<void>> = []; + + beforeEach(async () => { + const {defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + + extensionOptions = Object.assign({}, defaultBrowserOptions, { + args: [ + `--disable-extensions-except=${extensionPath}`, + `--load-extension=${extensionPath}`, + ], + }); + }); + + async function launchBrowser(options: typeof extensionOptions) { + const {browser, close} = await launch(options, {createContext: false}); + browsers.push(close); + return browser; + } + + afterEach(async () => { + await Promise.all( + browsers.map((close, index) => { + delete browsers[index]; + return close(); + }) + ); + }); + + it('background_page target type should be available', async () => { + const browserWithExtension = await launchBrowser(extensionOptions); + const page = await browserWithExtension.newPage(); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + target => { + return target.type() === 'background_page'; + } + ); + await page.close(); + await browserWithExtension.close(); + expect(backgroundPageTarget).toBeTruthy(); + }); + + it('service_worker target type should be available', async () => { + const browserWithExtension = await launchBrowser({ + args: [ + `--disable-extensions-except=${serviceWorkerExtensionPath}`, + `--load-extension=${serviceWorkerExtensionPath}`, + ], + }); + const page = await browserWithExtension.newPage(); + const serviceWorkerTarget = await browserWithExtension.waitForTarget( + target => { + return target.type() === 'service_worker'; + } + ); + await page.close(); + await browserWithExtension.close(); + expect(serviceWorkerTarget).toBeTruthy(); + }); + + it('target.page() should return a background_page', async function () { + const browserWithExtension = await launchBrowser(extensionOptions); + const backgroundPageTarget = await browserWithExtension.waitForTarget( + target => { + return target.type() === 'background_page'; + } + ); + const page = (await backgroundPageTarget.page())!; + expect( + await page.evaluate(() => { + return 2 * 3; + }) + ).toBe(6); + expect( + await page.evaluate(() => { + return (globalThis as any).MAGIC; + }) + ).toBe(42); + await browserWithExtension.close(); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/prerender.spec.ts b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts new file mode 100644 index 0000000000..4e0fb30da9 --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/prerender.spec.ts @@ -0,0 +1,181 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {statSync} from 'fs'; + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js'; +import {getUniqueVideoFilePlaceholder} from '../utils.js'; + +describe('Prerender', function () { + setupTestBrowserHooks(); + + it('can navigate to a prerendered page via input', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/prerender/index.html'); + + using button = await page.waitForSelector('button'); + await button?.click(); + + using link = await page.waitForSelector('a'); + await Promise.all([page.waitForNavigation(), link?.click()]); + expect( + await page.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + }); + + it('can navigate to a prerendered page via Puppeteer', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/prerender/index.html'); + + using button = await page.waitForSelector('button'); + await button?.click(); + + await page.goto(server.PREFIX + '/prerender/target.html'); + expect( + await page.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + }); + + describe('via frame', () => { + it('can navigate to a prerendered page via input', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/prerender/index.html'); + + using button = await page.waitForSelector('button'); + await button?.click(); + + const mainFrame = page.mainFrame(); + using link = await mainFrame.waitForSelector('a'); + await Promise.all([mainFrame.waitForNavigation(), link?.click()]); + expect(mainFrame).toBe(page.mainFrame()); + expect( + await mainFrame.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + expect(mainFrame).toBe(page.mainFrame()); + }); + + it('can navigate to a prerendered page via Puppeteer', async () => { + const {page, server} = await getTestState(); + await page.goto(server.PREFIX + '/prerender/index.html'); + + using button = await page.waitForSelector('button'); + await button?.click(); + + const mainFrame = page.mainFrame(); + await mainFrame.goto(server.PREFIX + '/prerender/target.html'); + expect( + await mainFrame.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + expect(mainFrame).toBe(page.mainFrame()); + }); + }); + + it('can screencast', async () => { + using file = getUniqueVideoFilePlaceholder(); + + const {page, server} = await getTestState(); + + const recorder = await page.screencast({ + path: file.filename, + scale: 0.5, + crop: {width: 100, height: 100, x: 0, y: 0}, + speed: 0.5, + }); + + await page.goto(server.PREFIX + '/prerender/index.html'); + + using button = await page.waitForSelector('button'); + await button?.click(); + + using link = await page.locator('a').waitHandle(); + await Promise.all([page.waitForNavigation(), link.click()]); + using input = await page.locator('input').waitHandle(); + await input.type('ab', {delay: 100}); + + await recorder.stop(); + + expect(statSync(file.filename).size).toBeGreaterThan(0); + }); + + describe('with network requests', () => { + it('can receive requests from the prerendered page', async () => { + const {page, server} = await getTestState(); + + const urls: string[] = []; + page.on('request', request => { + urls.push(request.url()); + }); + + await page.goto(server.PREFIX + '/prerender/index.html'); + using button = await page.waitForSelector('button'); + await button?.click(); + const mainFrame = page.mainFrame(); + using link = await mainFrame.waitForSelector('a'); + await Promise.all([mainFrame.waitForNavigation(), link?.click()]); + expect(mainFrame).toBe(page.mainFrame()); + expect( + await mainFrame.evaluate(() => { + return document.body.innerText; + }) + ).toBe('target'); + expect(mainFrame).toBe(page.mainFrame()); + expect( + urls.find(url => { + return url.endsWith('prerender/target.html'); + }) + ).toBeTruthy(); + expect( + urls.find(url => { + return url.includes('prerender/index.html'); + }) + ).toBeTruthy(); + expect( + urls.find(url => { + return url.includes('prerender/target.html?fromPrerendered'); + }) + ).toBeTruthy(); + }); + }); + + describe('with emulation', () => { + it('can configure viewport for prerendered pages', async () => { + const {page, server} = await getTestState(); + await page.setViewport({ + width: 300, + height: 400, + }); + await page.goto(server.PREFIX + '/prerender/index.html'); + using button = await page.waitForSelector('button'); + await button?.click(); + using link = await page.waitForSelector('a'); + await Promise.all([page.waitForNavigation(), link?.click()]); + const result = await page.evaluate(() => { + return { + width: document.documentElement.clientWidth, + height: document.documentElement.clientHeight, + dpr: window.devicePixelRatio, + }; + }); + expect({ + width: result.width, + height: result.height, + }).toStrictEqual({ + width: 300 * result.dpr, + height: 400 * result.dpr, + }); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts new file mode 100644 index 0000000000..405303fb6b --- /dev/null +++ b/remote/test/puppeteer/test/src/cdp/queryObjects.spec.ts @@ -0,0 +1,108 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from '../mocha-utils.js'; + +describe('page.queryObjects', function () { + setupTestBrowserHooks(); + + it('should work', async () => { + const {page} = await getTestState(); + + // Create a custom class + using classHandle = await page.evaluateHandle(() => { + return class CustomClass {}; + }); + + // Create an instance. + await page.evaluate(CustomClass => { + // @ts-expect-error: Different context. + self.customClass = new CustomClass(); + }, classHandle); + + // Validate only one has been added. + using prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + using objectsHandle = await page.queryObjects(prototypeHandle); + await expect( + page.evaluate(objects => { + return objects.length; + }, objectsHandle) + ).resolves.toBe(1); + + // Check that instances. + await expect( + page.evaluate(objects => { + // @ts-expect-error: Different context. + return objects[0] === self.customClass; + }, objectsHandle) + ).resolves.toBeTruthy(); + }); + it('should work for non-trivial page', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + + // Create a custom class + using classHandle = await page.evaluateHandle(() => { + return class CustomClass {}; + }); + + // Create an instance. + await page.evaluate(CustomClass => { + // @ts-expect-error: Different context. + self.customClass = new CustomClass(); + }, classHandle); + + // Validate only one has been added. + using prototypeHandle = await page.evaluateHandle(CustomClass => { + return CustomClass.prototype; + }, classHandle); + using objectsHandle = await page.queryObjects(prototypeHandle); + await expect( + page.evaluate(objects => { + return objects.length; + }, objectsHandle) + ).resolves.toBe(1); + + // Check that instances. + await expect( + page.evaluate(objects => { + // @ts-expect-error: Different context. + return objects[0] === self.customClass; + }, objectsHandle) + ).resolves.toBeTruthy(); + }); + it('should fail for disposed handles', async () => { + const {page} = await getTestState(); + + using prototypeHandle = await page.evaluateHandle(() => { + return HTMLBodyElement.prototype; + }); + // We want to dispose early. + await prototypeHandle.dispose(); + let error!: Error; + await page.queryObjects(prototypeHandle).catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Prototype JSHandle is disposed!'); + }); + it('should fail primitive values as prototypes', async () => { + const {page} = await getTestState(); + + using prototypeHandle = await page.evaluateHandle(() => { + return 42; + }); + let error!: Error; + await page.queryObjects(prototypeHandle).catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'Prototype JSHandle must not be referencing primitive value' + ); + }); +}); diff --git a/remote/test/puppeteer/test/src/chromiumonly.spec.ts b/remote/test/puppeteer/test/src/chromiumonly.spec.ts new file mode 100644 index 0000000000..e0c41317aa --- /dev/null +++ b/remote/test/puppeteer/test/src/chromiumonly.spec.ts @@ -0,0 +1,168 @@ +/** + * @license + * Copyright 2019 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import type {IncomingMessage} from 'http'; + +import expect from 'expect'; +import {Deferred} from 'puppeteer-core/internal/util/Deferred.js'; + +import {getTestState, setupTestBrowserHooks, launch} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; +// TODO: rename this test suite to launch/connect test suite as it actually +// works across browsers. +describe('Chromium-Specific Launcher tests', function () { + describe('Puppeteer.launch |browserURL| option', function () { + it('should be able to connect using browserUrl, with and without trailing slash', async () => { + const {close, puppeteer} = await launch({ + args: ['--remote-debugging-port=21222'], + }); + try { + const browserURL = 'http://127.0.0.1:21222'; + + const browser1 = await puppeteer.connect({browserURL}); + const page1 = await browser1.newPage(); + expect( + await page1.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + await browser1.disconnect(); + + const browser2 = await puppeteer.connect({ + browserURL: browserURL + '/', + }); + const page2 = await browser2.newPage(); + expect( + await page2.evaluate(() => { + return 8 * 7; + }) + ).toBe(56); + await browser2.disconnect(); + } finally { + await close(); + } + }); + it('should throw when using both browserWSEndpoint and browserURL', async () => { + const {browser, close, puppeteer} = await launch({ + args: ['--remote-debugging-port=21222'], + }); + try { + const browserURL = 'http://127.0.0.1:21222'; + + let error!: Error; + await puppeteer + .connect({ + browserURL, + browserWSEndpoint: browser.wsEndpoint(), + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Exactly one of browserWSEndpoint, browserURL or transport' + ); + } finally { + await close(); + } + }); + it('should throw when trying to connect to non-existing browser', async () => { + const {close, puppeteer} = await launch({ + args: ['--remote-debugging-port=21222'], + }); + try { + const browserURL = 'http://127.0.0.1:32333'; + + let error!: Error; + await puppeteer.connect({browserURL}).catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Failed to fetch browser webSocket URL from' + ); + } finally { + await close(); + } + }); + }); + + describe('Puppeteer.launch |pipe| option', function () { + it('should support the pipe option', async () => { + const {browser, close} = await launch({pipe: true}, {createPage: false}); + try { + expect(await browser.pages()).toHaveLength(1); + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + } finally { + await close(); + } + }); + it('should support the pipe argument', async () => { + const {defaultBrowserOptions} = await getTestState({skipLaunch: true}); + const options = Object.assign({}, defaultBrowserOptions); + options.args = ['--remote-debugging-pipe'].concat(options.args || []); + const {browser, close} = await launch(options); + try { + expect(browser.wsEndpoint()).toBe(''); + const page = await browser.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + } finally { + await close(); + } + }); + it('should fire "disconnected" when closing with pipe', async function () { + const {browser, close} = await launch({pipe: true}); + try { + const disconnectedEventPromise = waitEvent(browser, 'disconnected'); + // Emulate user exiting browser. + browser.process()!.kill(); + await Deferred.race([ + disconnectedEventPromise, + Deferred.create({ + message: `Failed in after Hook`, + timeout: this.timeout() - 1000, + }), + ]); + } finally { + await close(); + } + }); + }); +}); + +describe('Chromium-Specific Page Tests', function () { + setupTestBrowserHooks(); + + it('Page.setRequestInterception should work with intervention headers', async () => { + const {server, page} = await getTestState(); + + server.setRoute('/intervention', (_req, res) => { + return res.end(` + <script> + document.write('<script src="${server.CROSS_PROCESS_PREFIX}/intervention.js">' + '</scr' + 'ipt>'); + </script> + `); + }); + server.setRedirect('/intervention.js', '/redirect.js'); + let serverRequest: IncomingMessage | undefined; + server.setRoute('/redirect.js', (req, res) => { + serverRequest = req; + res.end('console.log(1);'); + }); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + await page.goto(server.PREFIX + '/intervention'); + // Check for feature URL substring rather than https://www.chromestatus.com to + // make it work with Edgium. + expect(serverRequest!.headers['intervention']).toContain( + 'feature/5718547946799104' + ); + }); +}); diff --git a/remote/test/puppeteer/test/src/click.spec.ts b/remote/test/puppeteer/test/src/click.spec.ts new file mode 100644 index 0000000000..cdc0e6c133 --- /dev/null +++ b/remote/test/puppeteer/test/src/click.spec.ts @@ -0,0 +1,478 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {KnownDevices} from 'puppeteer'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Page.click', function () { + setupTestBrowserHooks(); + + it('should click the button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should click svg', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <svg height="100" width="100"> + <circle onclick="javascript:window.__CLICKED=42" cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /> + </svg> + `); + await page.click('circle'); + expect( + await page.evaluate(() => { + return (globalThis as any).__CLICKED; + }) + ).toBe(42); + }); + it('should click the button if window.Node is removed', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + // @ts-expect-error Expected. + return delete window.Node; + }); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4281 + it('should click on a span with an inline element inside', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <style> + span::before { + content: 'q'; + } + </style> + <span onclick='javascript:window.CLICKED=42'></span> + `); + await page.click('span'); + expect( + await page.evaluate(() => { + return (globalThis as any).CLICKED; + }) + ).toBe(42); + }); + it('should not throw UnhandledPromiseRejection when page closes', async () => { + const {page} = await getTestState(); + + const newPage = await page.browser().newPage(); + await Promise.all([newPage.close(), newPage.mouse.click(1, 2)]).catch( + () => {} + ); + }); + it('should click the button after navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + await page.goto(server.PREFIX + '/input/button.html'); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should click with disabled javascript', async () => { + const {page, server} = await getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + await Promise.all([page.click('a'), page.waitForNavigation()]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + }); + it('should scroll and click with disabled javascript', async () => { + const {page, server} = await getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto(server.PREFIX + '/wrappedlink.html'); + using body = await page.waitForSelector('body'); + await body!.evaluate(el => { + el.style.paddingTop = '3000px'; + }); + await Promise.all([page.click('a'), page.waitForNavigation()]); + expect(page.url()).toBe(server.PREFIX + '/wrappedlink.html#clicked'); + }); + it('should click when one of inline box children is outside of viewport', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <style> + i { + position: absolute; + top: -1000px; + } + </style> + <span onclick='javascript:window.CLICKED = 42;'><i>woof</i><b>doggo</b></span> + `); + await page.click('span'); + expect( + await page.evaluate(() => { + return (globalThis as any).CLICKED; + }) + ).toBe(42); + }); + it('should select the text by triple clicking', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + await page.keyboard.type(text); + await page.evaluate(() => { + (window as any).clicks = []; + window.addEventListener('click', event => { + return (window as any).clicks.push(event.detail); + }); + }); + await page.click('textarea', {count: 3}); + expect( + await page.evaluate(() => { + return (window as any).clicks; + }) + ).toMatchObject({0: 1, 1: 2, 2: 3}); + expect( + await page.evaluate(() => { + const textarea = document.querySelector('textarea'); + return textarea!.value.substring( + textarea!.selectionStart, + textarea!.selectionEnd + ); + }) + ).toBe(text); + }); + it('should click offscreen buttons', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const messages: string[] = []; + page.on('console', msg => { + if (msg.type() === 'log') { + return messages.push(msg.text()); + } + return; + }); + for (let i = 0; i < 11; ++i) { + // We might've scrolled to click a button - reset to (0, 0). + await page.evaluate(() => { + return window.scrollTo(0, 0); + }); + await page.click(`#btn${i}`); + } + expect(messages).toEqual([ + 'button #0 clicked', + 'button #1 clicked', + 'button #2 clicked', + 'button #3 clicked', + 'button #4 clicked', + 'button #5 clicked', + 'button #6 clicked', + 'button #7 clicked', + 'button #8 clicked', + 'button #9 clicked', + 'button #10 clicked', + ]); + }); + + it('should click wrapped links', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/wrappedlink.html'); + await page.click('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).__clicked; + }) + ).toBe(true); + }); + + it('should click on checkbox input and toggle', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(null); + await page.click('input#agree'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).result.events; + }) + ).toEqual([ + 'mouseover', + 'mouseenter', + 'mousemove', + 'mousedown', + 'mouseup', + 'click', + 'input', + 'change', + ]); + await page.click('input#agree'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(false); + }); + + it('should click on checkbox label and toggle', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/checkbox.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(null); + await page.click('label[for="agree"]'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return (globalThis as any).result.events; + }) + ).toEqual(['click', 'input', 'change']); + await page.click('label[for="agree"]'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.check; + }) + ).toBe(false); + }); + + it('should fail to click a missing button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + let error!: Error; + await page.click('button.does-not-exist').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'No element found for selector: button.does-not-exist' + ); + }); + // @see https://github.com/puppeteer/puppeteer/issues/161 + it('should not hang with touch-enabled viewports', async () => { + const {page} = await getTestState(); + + await page.setViewport(KnownDevices['iPhone 6'].viewport); + await page.mouse.down(); + await page.mouse.move(100, 10); + await page.mouse.up(); + }); + it('should scroll and click the button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-5'); + expect( + await page.evaluate(() => { + return document.querySelector('#button-5')!.textContent; + }) + ).toBe('clicked'); + await page.click('#button-80'); + expect( + await page.evaluate(() => { + return document.querySelector('#button-80')!.textContent; + }) + ).toBe('clicked'); + }); + it('should double click the button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + (globalThis as any).double = false; + const button = document.querySelector('button'); + button!.addEventListener('dblclick', () => { + (globalThis as any).double = true; + }); + }); + using button = (await page.$('button'))!; + await button!.click({count: 2}); + expect(await page.evaluate('double')).toBe(true); + expect(await page.evaluate('result')).toBe('Clicked'); + }); + it('should click a partially obscured button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + await page.evaluate(() => { + const button = document.querySelector('button'); + button!.textContent = 'Some really long text that will go offscreen'; + button!.style.position = 'absolute'; + button!.style.left = '368px'; + }); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should click a rotated button', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/rotatedButton.html'); + await page.click('button'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should fire contextmenu event on right click', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', {button: 'right'}); + expect( + await page.evaluate(() => { + return document.querySelector('#button-8')!.textContent; + }) + ).toBe('context menu'); + }); + it('should fire aux event on middle click', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', {button: 'middle'}); + expect( + await page.evaluate(() => { + return document.querySelector('#button-8')!.textContent; + }) + ).toBe('aux click'); + }); + it('should fire back click', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', {button: 'back'}); + expect( + await page.evaluate(() => { + return document.querySelector('#button-8')!.textContent; + }) + ).toBe('back click'); + }); + it('should fire forward click', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.click('#button-8', {button: 'forward'}); + expect( + await page.evaluate(() => { + return document.querySelector('#button-8')!.textContent; + }) + ).toBe('forward click'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/206 + it('should click links which cause navigation', async () => { + const {page, server} = await getTestState(); + + await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`); + // This await should not hang. + await page.click('a'); + }); + it('should click the button inside an iframe', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('<div style="width:100px;height:100px">spacer</div>'); + await attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + using button = await frame!.$('button'); + await button!.click(); + expect( + await frame!.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4110 + it('should click the button with fixed position inside an iframe', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 500, height: 500}); + await page.setContent( + '<div style="width:100px;height:2000px">spacer</div>' + ); + await attachFrame( + page, + 'button-test', + server.CROSS_PROCESS_PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + await frame!.$eval('button', (button: Element) => { + return (button as HTMLElement).style.setProperty('position', 'fixed'); + }); + await frame!.click('button'); + expect( + await frame!.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should click the button with deviceScaleFactor set', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 400, height: 400, deviceScaleFactor: 5}); + expect( + await page.evaluate(() => { + return window.devicePixelRatio; + }) + ).toBe(5); + await page.setContent('<div style="width:100px;height:100px">spacer</div>'); + await attachFrame( + page, + 'button-test', + server.PREFIX + '/input/button.html' + ); + const frame = page.frames()[1]; + using button = await frame!.$('button'); + await button!.click(); + expect( + await frame!.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); +}); diff --git a/remote/test/puppeteer/test/src/cookies.spec.ts b/remote/test/puppeteer/test/src/cookies.spec.ts new file mode 100644 index 0000000000..f232831b72 --- /dev/null +++ b/remote/test/puppeteer/test/src/cookies.spec.ts @@ -0,0 +1,557 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; + +import { + expectCookieEquals, + getTestState, + launch, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('Cookie specs', () => { + setupTestBrowserHooks(); + + describe('Page.cookies', function () { + it('should return no cookies in pristine browser context', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await expectCookieEquals(await page.cookies(), []); + }); + it('should get a cookie', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + + await expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('should properly report httpOnly cookie', async () => { + const {page, server} = await getTestState(); + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('Set-Cookie', 'a=b; HttpOnly; Path=/'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies).toHaveLength(1); + expect(cookies[0]!.httpOnly).toBe(true); + }); + it('should properly report "Strict" sameSite cookie', async () => { + const {page, server} = await getTestState(); + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Strict'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies).toHaveLength(1); + expect(cookies[0]!.sameSite).toBe('Strict'); + }); + it('should properly report "Lax" sameSite cookie', async () => { + const {page, server} = await getTestState(); + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('Set-Cookie', 'a=b; SameSite=Lax'); + res.end(); + }); + await page.goto(server.EMPTY_PAGE); + const cookies = await page.cookies(); + expect(cookies).toHaveLength(1); + expect(cookies[0]!.sameSite).toBe('Lax'); + }); + it('should get multiple cookies', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + document.cookie = 'password=1234'; + }); + const cookies = await page.cookies(); + cookies.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + await expectCookieEquals(cookies, [ + { + name: 'password', + value: '1234', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 12, + httpOnly: false, + secure: false, + session: true, + sourceScheme: 'NonSecure', + }, + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('should get cookies from multiple urls', async () => { + const {page} = await getTestState(); + await page.setCookie( + { + url: 'https://foo.com', + name: 'doggo', + value: 'woofs', + }, + { + url: 'https://bar.com', + name: 'catto', + value: 'purrs', + }, + { + url: 'https://baz.com', + name: 'birdo', + value: 'tweets', + } + ); + const cookies = await page.cookies('https://foo.com', 'https://baz.com'); + cookies.sort((a, b) => { + return a.name.localeCompare(b.name); + }); + await expectCookieEquals(cookies, [ + { + name: 'birdo', + value: 'tweets', + domain: 'baz.com', + path: '/', + sameParty: false, + expires: -1, + size: 11, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + { + name: 'doggo', + value: 'woofs', + domain: 'foo.com', + path: '/', + sameParty: false, + expires: -1, + size: 10, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ]); + }); + }); + describe('Page.setCookie', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + expect( + await page.evaluate(() => { + return document.cookie; + }) + ).toEqual('password=123456'); + }); + it('should isolate cookies in browser contexts', async () => { + const {page, server, browser} = await getTestState(); + + const anotherContext = await browser.createIncognitoBrowserContext(); + const anotherPage = await anotherContext.newPage(); + + await page.goto(server.EMPTY_PAGE); + await anotherPage.goto(server.EMPTY_PAGE); + + await page.setCookie({name: 'page1cookie', value: 'page1value'}); + await anotherPage.setCookie({name: 'page2cookie', value: 'page2value'}); + + const cookies1 = await page.cookies(); + const cookies2 = await anotherPage.cookies(); + expect(cookies1).toHaveLength(1); + expect(cookies2).toHaveLength(1); + expect(cookies1[0]!.name).toBe('page1cookie'); + expect(cookies1[0]!.value).toBe('page1value'); + expect(cookies2[0]!.name).toBe('page2cookie'); + expect(cookies2[0]!.value).toBe('page2value'); + await anotherContext.close(); + }); + it('should set multiple cookies', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'password', + value: '123456', + }, + { + name: 'foo', + value: 'bar', + } + ); + const cookieStrings = await page.evaluate(() => { + const cookies = document.cookie.split(';'); + return cookies + .map(cookie => { + return cookie.trim(); + }) + .sort(); + }); + + expect(cookieStrings).toEqual(['foo=bar', 'password=123456']); + }); + it('should have |expires| set to |-1| for session cookies', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + expect(cookies[0]!.session).toBe(true); + expect(cookies[0]!.expires).toBe(-1); + }); + it('should set cookie with reasonable defaults', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'password', + value: '123456', + }); + const cookies = await page.cookies(); + await expectCookieEquals( + cookies.sort((a, b) => { + return a.name.localeCompare(b.name); + }), + [ + { + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ] + ); + }); + it('should set a cookie with a path', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html', + }); + await expectCookieEquals(await page.cookies(), [ + { + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + sameParty: false, + expires: -1, + size: 14, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(server.EMPTY_PAGE); + await expectCookieEquals(await page.cookies(), []); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto(server.PREFIX + '/grid.html'); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + }); + it('should not set a cookie on a blank page', async () => { + const {page} = await getTestState(); + + await page.goto('about:blank'); + let error!: Error; + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it('should not set a cookie with blank page URL', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + await page.goto(server.EMPTY_PAGE); + try { + await page.setCookie( + {name: 'example-cookie', value: 'best'}, + {url: 'about:blank', name: 'example-cookie-blank', value: 'best'} + ); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toEqual( + `Blank page can not have cookie "example-cookie-blank"` + ); + }); + it('should not set a cookie on a data URL page', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page.goto('data:,Hello%2C%20World!'); + try { + await page.setCookie({name: 'example-cookie', value: 'best'}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain( + 'At least one of the url and domain needs to be specified' + ); + }); + it('should default to setting secure cookie for HTTPS websites', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const SECURE_URL = 'https://example.com'; + await page.setCookie({ + url: SECURE_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(SECURE_URL); + expect(cookie!.secure).toBe(true); + }); + it('should be able to set insecure cookie for HTTP website', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const HTTP_URL = 'http://example.com'; + await page.setCookie({ + url: HTTP_URL, + name: 'foo', + value: 'bar', + }); + const [cookie] = await page.cookies(HTTP_URL); + expect(cookie!.secure).toBe(false); + }); + it('should set a cookie on a different domain', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + url: 'https://www.example.com', + name: 'example-cookie', + value: 'best', + }); + expect(await page.evaluate('document.cookie')).toBe(''); + await expectCookieEquals(await page.cookies(), []); + await expectCookieEquals(await page.cookies('https://www.example.com'), [ + { + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + sameParty: false, + expires: -1, + size: 18, + httpOnly: false, + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ]); + }); + it('should set cookies from a frame', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/grid.html'); + await page.setCookie({name: 'localhost-cookie', value: 'best'}); + await page.evaluate(src => { + let fulfill!: () => void; + const promise = new Promise<void>(x => { + return (fulfill = x); + }); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, server.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-cookie', + value: 'worst', + url: server.CROSS_PROCESS_PREFIX, + }); + expect(await page.evaluate('document.cookie')).toBe( + 'localhost-cookie=best' + ); + expect(await page.frames()[1]!.evaluate('document.cookie')).toBe(''); + + await expectCookieEquals(await page.cookies(), [ + { + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 20, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + + await expectCookieEquals( + await page.cookies(server.CROSS_PROCESS_PREFIX), + [ + { + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + sameParty: false, + expires: -1, + size: 15, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ] + ); + }); + it('should set secure same-site cookies from a frame', async () => { + const {httpsServer, browser, close} = await launch({ + ignoreHTTPSErrors: true, + }); + + try { + const page = await browser.newPage(); + await page.goto(httpsServer.PREFIX + '/grid.html'); + await page.evaluate(src => { + let fulfill!: () => void; + const promise = new Promise<void>(x => { + return (fulfill = x); + }); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, httpsServer.CROSS_PROCESS_PREFIX); + await page.setCookie({ + name: '127-same-site-cookie', + value: 'best', + url: httpsServer.CROSS_PROCESS_PREFIX, + sameSite: 'None', + }); + + expect(await page.frames()[1]!.evaluate('document.cookie')).toBe( + '127-same-site-cookie=best' + ); + await expectCookieEquals( + await page.cookies(httpsServer.CROSS_PROCESS_PREFIX), + [ + { + name: '127-same-site-cookie', + value: 'best', + domain: '127.0.0.1', + path: '/', + sameParty: false, + expires: -1, + size: 24, + httpOnly: false, + sameSite: 'None', + secure: true, + session: true, + sourcePort: 443, + sourceScheme: 'Secure', + }, + ] + ); + } finally { + await close(); + } + }); + }); + + describe('Page.deleteCookie', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + }, + { + name: 'cookie3', + value: '3', + } + ); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie2=2; cookie3=3' + ); + await page.deleteCookie({name: 'cookie2'}); + expect(await page.evaluate('document.cookie')).toBe( + 'cookie1=1; cookie3=3' + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/coverage.spec.ts b/remote/test/puppeteer/test/src/coverage.spec.ts new file mode 100644 index 0000000000..6a95db541c --- /dev/null +++ b/remote/test/puppeteer/test/src/coverage.spec.ts @@ -0,0 +1,343 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Coverage specs', function () { + setupTestBrowserHooks(); + + describe('JSCoverage', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'load', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toContain('/jscoverage/simple.html'); + expect(coverage[0]!.ranges).toEqual([ + {start: 0, end: 17}, + {start: 35, end: 61}, + ]); + }); + it('should report sourceURLs', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/sourceurl.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toBe('nicename.js'); + }); + it('should ignore eval() scripts by default', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + }); + it('should not ignore eval() scripts if reportAnonymousScripts is true', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + coverage.find(entry => { + return entry.url.startsWith('debugger://'); + }) + ).not.toBe(null); + expect(coverage).toHaveLength(2); + }); + it('should ignore pptr internal scripts if reportAnonymousScripts is true', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => { + return console.log('bar'); + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(0); + }); + it('should report multiple scripts', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(2); + coverage.sort((a, b) => { + return a.url.localeCompare(b.url); + }); + expect(coverage[0]!.url).toContain('/jscoverage/script1.js'); + expect(coverage[1]!.url).toContain('/jscoverage/script2.js'); + }); + it('should report right ranges', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.ranges).toHaveLength(2); + const range1 = entry.ranges[0]!; + expect(entry.text.substring(range1.start, range1.end)).toBe('\n'); + const range2 = entry.ranges[1]!; + expect(entry.text.substring(range2.start, range2.end)).toBe( + `console.log('used!');if(true===false)` + ); + }); + it('should report right ranges for "per function" scope', async () => { + const {page, server} = await getTestState(); + + const coverageOptions = { + useBlockCoverage: false, + }; + + await page.coverage.startJSCoverage(coverageOptions); + await page.goto(server.PREFIX + '/jscoverage/ranges.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.ranges).toHaveLength(2); + const range1 = entry.ranges[0]!; + expect(entry.text.substring(range1.start, range1.end)).toBe('\n'); + const range2 = entry.ranges[1]!; + expect(entry.text.substring(range2.start, range2.end)).toBe( + `console.log('used!');if(true===false)console.log('unused!');` + ); + }); + it('should report scripts that have no coverage', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/unused.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + const entry = coverage[0]!; + expect(entry.url).toContain('unused.html'); + expect(entry.ranges).toHaveLength(0); + }); + it('should work with conditionals', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/involved.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/') + ).toBeGolden('jscoverage-involved.txt'); + }); + // @see https://crbug.com/990945 + it.skip('should not hang when there is a debugger statement', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + describe('resetOnNavigation', function () { + it('should report scripts across navigations when disabled', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + // TODO: navigating too fast might loose JS coverage data in the browser. + await page.waitForNetworkIdle(); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(2); + }); + + it('should NOT report scripts across navigations when enabled', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/jscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(0); + }); + }); + describe('includeRawScriptCoverage', function () { + it('should not include rawScriptCoverage field when disabled', async () => { + const {page, server} = await getTestState(); + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'load', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.rawScriptCoverage).toBeUndefined(); + }); + it('should include rawScriptCoverage field when enabled', async () => { + const {page, server} = await getTestState(); + await page.coverage.startJSCoverage({ + includeRawScriptCoverage: true, + }); + await page.goto(server.PREFIX + '/jscoverage/simple.html', { + waitUntil: 'load', + }); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.rawScriptCoverage).toBeTruthy(); + }); + }); + // @see https://crbug.com/990945 + it.skip('should not hang when there is a debugger statement', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startJSCoverage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + debugger; // eslint-disable-line no-debugger + }); + await page.coverage.stopJSCoverage(); + }); + }); + + describe('CSSCoverage', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/simple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toContain('/csscoverage/simple.html'); + expect(coverage[0]!.ranges).toEqual([{start: 1, end: 22}]); + const range = coverage[0]!.ranges[0]!; + expect(coverage[0]!.text.substring(range.start, range.end)).toBe( + 'div { color: green; }' + ); + }); + it('should report sourceURLs', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/sourceurl.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toBe('nicename.css'); + }); + it('should report multiple stylesheets', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(2); + coverage.sort((a, b) => { + return a.url.localeCompare(b.url); + }); + expect(coverage[0]!.url).toContain('/csscoverage/stylesheet1.css'); + expect(coverage[1]!.url).toContain('/csscoverage/stylesheet2.css'); + }); + it('should report stylesheets that have no coverage', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/unused.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toBe('unused.css'); + expect(coverage[0]!.ranges).toHaveLength(0); + }); + it('should work with media queries', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/media.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.url).toContain('/csscoverage/media.html'); + expect(coverage[0]!.ranges).toEqual([ + {start: 8, end: 15}, + {start: 17, end: 38}, + ]); + }); + it('should work with complicated usecases', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/involved.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect( + JSON.stringify(coverage, null, 2).replace(/:\d{4,5}\//g, ':<PORT>/') + ).toBeGolden('csscoverage-involved.txt'); + }); + it('should work with empty stylesheets', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.goto(server.PREFIX + '/csscoverage/empty.html'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + expect(coverage[0]!.text).toEqual(''); + }); + it('should ignore injected stylesheets', async () => { + const {page} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.addStyleTag({content: 'body { margin: 10px;}'}); + // trigger style recalc + const margin = await page.evaluate(() => { + return window.getComputedStyle(document.body).margin; + }); + expect(margin).toBe('10px'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(0); + }); + it('should work with a recently loaded stylesheet', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); + await page.evaluate(async url => { + document.body.textContent = 'hello, world'; + + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = url; + document.head.appendChild(link); + await new Promise(x => { + return (link.onload = x); + }); + }, server.PREFIX + '/csscoverage/stylesheet1.css'); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(1); + }); + describe('resetOnNavigation', function () { + it('should report stylesheets across navigations', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage({resetOnNavigation: false}); + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(2); + }); + it('should NOT report scripts across navigations', async () => { + const {page, server} = await getTestState(); + + await page.coverage.startCSSCoverage(); // Enabled by default. + await page.goto(server.PREFIX + '/csscoverage/multiple.html'); + await page.goto(server.EMPTY_PAGE); + const coverage = await page.coverage.stopCSSCoverage(); + expect(coverage).toHaveLength(0); + }); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/debugInfo.spec.ts b/remote/test/puppeteer/test/src/debugInfo.spec.ts new file mode 100644 index 0000000000..079107cab7 --- /dev/null +++ b/remote/test/puppeteer/test/src/debugInfo.spec.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2024 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('DebugInfo', function () { + setupTestBrowserHooks(); + + describe('Browser.debugInfo', function () { + it('should work', async () => { + const {page, browser} = await getTestState(); + + const promise = page.evaluate(() => { + return new Promise(resolve => { + // @ts-expect-error another context + window.resolve = resolve; + }); + }); + try { + expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(1); + } finally { + await page.evaluate(() => { + // @ts-expect-error another context + window.resolve(); + }); + } + await promise; + expect(browser.debugInfo.pendingProtocolErrors).toHaveLength(0); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts new file mode 100644 index 0000000000..69a5a069af --- /dev/null +++ b/remote/test/puppeteer/test/src/defaultbrowsercontext.spec.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; + +import { + expectCookieEquals, + getTestState, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('DefaultBrowserContext', function () { + setupTestBrowserHooks(); + + it('page.cookies() should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + await expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('page.setCookie() should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie({ + name: 'username', + value: 'John Doe', + }); + expect( + await page.evaluate(() => { + return document.cookie; + }) + ).toBe('username=John Doe'); + await expectCookieEquals(await page.cookies(), [ + { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 16, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + }); + it('page.deleteCookie() should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setCookie( + { + name: 'cookie1', + value: '1', + }, + { + name: 'cookie2', + value: '2', + } + ); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + await page.deleteCookie({name: 'cookie2'}); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + await expectCookieEquals(await page.cookies(), [ + { + name: 'cookie1', + value: '1', + domain: 'localhost', + path: '/', + sameParty: false, + expires: -1, + size: 8, + httpOnly: false, + secure: false, + session: true, + sourcePort: 80, + sourceScheme: 'NonSecure', + }, + ]); + }); +}); diff --git a/remote/test/puppeteer/test/src/device-request-prompt.spec.ts b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts new file mode 100644 index 0000000000..e6e2cdd65e --- /dev/null +++ b/remote/test/puppeteer/test/src/device-request-prompt.spec.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; + +import {launch} from './mocha-utils.js'; + +describe('device request prompt', function () { + let state: Awaited<ReturnType<typeof launch>>; + + before(async () => { + state = await launch( + { + args: ['--enable-features=WebBluetoothNewPermissionsBackend'], + ignoreHTTPSErrors: true, + }, + { + after: 'all', + } + ); + }); + + after(async () => { + await state.close(); + }); + + beforeEach(async () => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context.close(); + }); + + // Bug: #11072 + it('does not crash', async function () { + this.timeout(1_000); + + const {page, httpsServer} = state; + + await page.goto(httpsServer.EMPTY_PAGE); + + await expect( + page.waitForDevicePrompt({ + timeout: 10, + }) + ).rejects.toThrow(TimeoutError); + }); +}); diff --git a/remote/test/puppeteer/test/src/dialog.spec.ts b/remote/test/puppeteer/test/src/dialog.spec.ts new file mode 100644 index 0000000000..e137ccf517 --- /dev/null +++ b/remote/test/puppeteer/test/src/dialog.spec.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; +import sinon from 'sinon'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Page.Events.Dialog', function () { + setupTestBrowserHooks(); + + it('should fire', async () => { + const {page} = await getTestState(); + + const onDialog = sinon.stub().callsFake(dialog => { + dialog.accept(); + }); + page.on('dialog', onDialog); + + await page.evaluate(() => { + return alert('yo'); + }); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]!; + expect(dialog.type()).toBe('alert'); + expect(dialog.defaultValue()).toBe(''); + expect(dialog.message()).toBe('yo'); + }); + + it('should allow accepting prompts', async () => { + const {page} = await getTestState(); + + const onDialog = sinon.stub().callsFake(dialog => { + dialog.accept('answer!'); + }); + page.on('dialog', onDialog); + + const result = await page.evaluate(() => { + return prompt('question?', 'yes.'); + }); + + expect(onDialog.callCount).toEqual(1); + const dialog = onDialog.firstCall.args[0]!; + expect(dialog.type()).toBe('prompt'); + expect(dialog.defaultValue()).toBe('yes.'); + expect(dialog.message()).toBe('question?'); + + expect(result).toBe('answer!'); + }); + it('should dismiss the prompt', async () => { + const {page} = await getTestState(); + + page.on('dialog', dialog => { + void dialog.dismiss(); + }); + const result = await page.evaluate(() => { + return prompt('question?'); + }); + expect(result).toBe(null); + }); +}); diff --git a/remote/test/puppeteer/test/src/diffstyle.css b/remote/test/puppeteer/test/src/diffstyle.css new file mode 100644 index 0000000000..202e85f41a --- /dev/null +++ b/remote/test/puppeteer/test/src/diffstyle.css @@ -0,0 +1,13 @@ +body { + font-family: monospace; + white-space: pre; +} + +ins { + background-color: #9cffa0; + text-decoration: none; +} + +del { + background-color: #ff9e9e; +} diff --git a/remote/test/puppeteer/test/src/drag-and-drop.spec.ts b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts new file mode 100644 index 0000000000..cfe18b55a4 --- /dev/null +++ b/remote/test/puppeteer/test/src/drag-and-drop.spec.ts @@ -0,0 +1,154 @@ +/** + * @license + * Copyright 2021 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +async function getDragState() { + const {page} = await getTestState({skipLaunch: true}); + return parseInt( + await page.$eval('#drag-state', element => { + return element.innerHTML; + }), + 10 + ); +} + +describe("Legacy Drag n' Drop", function () { + setupTestBrowserHooks(); + + it('should emit a dragIntercepted event when dragged', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + using draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + + assert(data instanceof Object); + expect(data.items).toHaveLength(1); + expect(await getDragState()).toBe(1); + }); + it('should emit a dragEnter', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + using draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + assert(data instanceof Object); + using dropzone = (await page.$('#drop'))!; + await dropzone.dragEnter(data); + + expect(await getDragState()).toBe(12); + }); + it('should emit a dragOver event', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + using draggable = (await page.$('#drag'))!; + const data = await draggable.drag({x: 1, y: 1}); + assert(data instanceof Object); + using dropzone = (await page.$('#drop'))!; + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + + expect(await getDragState()).toBe(123); + }); + it('can be dropped', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + using draggable = (await page.$('#drag'))!; + using dropzone = (await page.$('#drop'))!; + const data = await draggable.drag({x: 1, y: 1}); + assert(data instanceof Object); + await dropzone.dragEnter(data); + await dropzone.dragOver(data); + await dropzone.drop(data); + + expect(await getDragState()).toBe(12334); + }); + it('can be dragged and dropped with a single function', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + expect(page.isDragInterceptionEnabled()).toBe(false); + await page.setDragInterception(true); + expect(page.isDragInterceptionEnabled()).toBe(true); + using draggable = (await page.$('#drag'))!; + using dropzone = (await page.$('#drop'))!; + await draggable.dragAndDrop(dropzone); + + expect(await getDragState()).toBe(12334); + }); +}); + +describe("Drag n' Drop", () => { + setupTestBrowserHooks(); + + it('should drop', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + + using draggable = await page.$('#drag'); + assert(draggable); + using dropzone = await page.$('#drop'); + assert(dropzone); + + await dropzone.drop(draggable); + + expect(await getDragState()).toBe(1234); + }); + it('should drop using mouse', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + + using draggable = await page.$('#drag'); + assert(draggable); + using dropzone = await page.$('#drop'); + assert(dropzone); + + await draggable.hover(); + await page.mouse.down(); + await dropzone.hover(); + + expect(await getDragState()).toBe(123); + + await page.mouse.up(); + expect(await getDragState()).toBe(1234); + }); + it('should drag and drop', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/drag-and-drop.html'); + + using draggable = await page.$('#drag'); + assert(draggable); + using dropzone = await page.$('#drop'); + assert(dropzone); + + await draggable.drag(dropzone); + await dropzone.drop(draggable); + + expect(await getDragState()).toBe(1234); + }); +}); diff --git a/remote/test/puppeteer/test/src/elementhandle.spec.ts b/remote/test/puppeteer/test/src/elementhandle.spec.ts new file mode 100644 index 0000000000..9aaf914224 --- /dev/null +++ b/remote/test/puppeteer/test/src/elementhandle.spec.ts @@ -0,0 +1,953 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer'; +import {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; +import { + asyncDisposeSymbol, + disposeSymbol, +} from 'puppeteer-core/internal/util/disposable.js'; +import sinon from 'sinon'; + +import { + getTestState, + setupTestBrowserHooks, + shortWaitForArrayToHaveAtLeastNElements, +} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('ElementHandle specs', function () { + setupTestBrowserHooks(); + + describe('ElementHandle.boundingBox', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + using elementHandle = (await page.$('.box:nth-of-type(13)'))!; + const box = await elementHandle.boundingBox(); + expect(box).toEqual({x: 100, y: 50, width: 50, height: 50}); + }); + it('should handle nested frames', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const nestedFrame = page.frames()[1]!.childFrames()[1]!; + using elementHandle = (await nestedFrame.$('div'))!; + const box = await elementHandle.boundingBox(); + if (isChrome) { + expect(box).toEqual({x: 28, y: 182, width: 264, height: 18}); + } else { + expect(box).toEqual({x: 28, y: 182, width: 254, height: 18}); + } + }); + it('should return null for invisible elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div style="display:none">hi</div>'); + using element = (await page.$('div'))!; + expect(await element.boundingBox()).toBe(null); + }); + it('should force a layout', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent( + '<div style="width: 100px; height: 100px">hello</div>' + ); + using elementHandle = (await page.$('div'))!; + await page.evaluate((element: HTMLElement) => { + return (element.style.height = '200px'); + }, elementHandle); + const box = await elementHandle.boundingBox(); + expect(box).toEqual({x: 8, y: 8, width: 100, height: 200}); + }); + it('should work with SVG nodes', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> + <rect id="theRect" x="30" y="50" width="200" height="300"></rect> + </svg> + `); + using element = (await page.$( + '#therect' + )) as ElementHandle<SVGRectElement>; + const pptrBoundingBox = await element.boundingBox(); + const webBoundingBox = await page.evaluate(e => { + const rect = e.getBoundingClientRect(); + return {x: rect.x, y: rect.y, width: rect.width, height: rect.height}; + }, element); + expect(pptrBoundingBox).toEqual(webBoundingBox); + }); + }); + + describe('ElementHandle.boxModel', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/resetcss.html'); + + // Step 1: Add Frame and position it absolutely. + await attachFrame(page, 'frame1', server.PREFIX + '/resetcss.html'); + await page.evaluate(() => { + const frame = document.querySelector<HTMLElement>('#frame1')!; + frame.style.position = 'absolute'; + frame.style.left = '1px'; + frame.style.top = '2px'; + }); + + // Step 2: Add div and position it absolutely inside frame. + const frame = page.frames()[1]!; + using divHandle = ( + await frame.evaluateHandle(() => { + const div = document.createElement('div'); + document.body.appendChild(div); + div.style.boxSizing = 'border-box'; + div.style.position = 'absolute'; + div.style.borderLeft = '1px solid black'; + div.style.paddingLeft = '2px'; + div.style.marginLeft = '3px'; + div.style.left = '4px'; + div.style.top = '5px'; + div.style.width = '6px'; + div.style.height = '7px'; + return div; + }) + ).asElement()!; + + // Step 3: query div's boxModel and assert box values. + const box = (await divHandle.boxModel())!; + expect(box.width).toBe(6); + expect(box.height).toBe(7); + expect(box.margin[0]).toEqual({ + x: 1 + 4, // frame.left + div.left + y: 2 + 5, + }); + expect(box.border[0]).toEqual({ + x: 1 + 4 + 3, // frame.left + div.left + div.margin-left + y: 2 + 5, + }); + expect(box.padding[0]).toEqual({ + x: 1 + 4 + 3 + 1, // frame.left + div.left + div.marginLeft + div.borderLeft + y: 2 + 5, + }); + expect(box.content[0]).toEqual({ + x: 1 + 4 + 3 + 1 + 2, // frame.left + div.left + div.marginLeft + div.borderLeft + div.paddingLeft + y: 2 + 5, + }); + }); + + it('should return null for invisible elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div style="display:none">hi</div>'); + using element = (await page.$('div'))!; + expect(await element.boxModel()).toBe(null); + }); + }); + + describe('ElementHandle.contentFrame', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + using elementHandle = (await page.$('#frame1'))!; + const frame = await elementHandle.contentFrame(); + expect(frame).toBe(page.frames()[1]); + }); + }); + + describe('ElementHandle.isVisible and ElementHandle.isHidden', function () { + it('should work', async () => { + const {page} = await getTestState(); + await page.setContent('<div style="display: none">text</div>'); + using element = (await page.waitForSelector('div'))!; + await expect(element.isVisible()).resolves.toBeFalsy(); + await expect(element.isHidden()).resolves.toBeTruthy(); + await element.evaluate(e => { + e.style.removeProperty('display'); + }); + await expect(element.isVisible()).resolves.toBeTruthy(); + await expect(element.isHidden()).resolves.toBeFalsy(); + }); + }); + + describe('ElementHandle.click', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + using button = (await page.$('button'))!; + await button.click(); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + it('should return Point data', async () => { + const {page} = await getTestState(); + + const clicks: Array<[x: number, y: number]> = []; + + await page.exposeFunction('reportClick', (x: number, y: number): void => { + clicks.push([x, y]); + }); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` + <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div> + `; + document.body.addEventListener('click', e => { + (window as any).reportClick(e.clientX, e.clientY); + }); + }); + + using divHandle = (await page.$('div'))!; + await divHandle.click(); + await divHandle.click({ + offset: { + x: 10, + y: 15, + }, + }); + await shortWaitForArrayToHaveAtLeastNElements(clicks, 2); + expect(clicks).toEqual([ + [45 + 60, 45 + 30], // margin + middle point offset + [30 + 10, 30 + 15], // margin + offset + ]); + }); + it('should work for Shadow DOM v1', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + using buttonHandle = await page.evaluateHandle(() => { + // @ts-expect-error button is expected to be in the page's scope. + return button as HTMLButtonElement; + }); + await buttonHandle.click(); + expect( + await page.evaluate(() => { + // @ts-expect-error clicked is expected to be in the page's scope. + return clicked; + }) + ).toBe(true); + }); + it('should not work for TextNodes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + using buttonTextNode = await page.evaluateHandle(() => { + return document.querySelector('button')!.firstChild as HTMLElement; + }); + let error!: Error; + await buttonTextNode.click().catch(error_ => { + return (error = error_); + }); + expect(error.message).atLeastOneToContain([ + 'Node is not of type HTMLElement', + 'no such node', + ]); + }); + it('should throw for detached nodes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + using button = (await page.$('button'))!; + await page.evaluate((button: HTMLElement) => { + return button.remove(); + }, button); + let error!: Error; + await button.click().catch(error_ => { + return (error = error_); + }); + expect(error.message).atLeastOneToContain([ + 'Node is detached from document', + 'no such node', + ]); + }); + it('should throw for hidden nodes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + using button = (await page.$('button'))!; + await page.evaluate((button: HTMLElement) => { + return (button.style.display = 'none'); + }, button); + const error = await button.click().catch(error_ => { + return error_; + }); + expect(error.message).atLeastOneToContain([ + 'Node is either not clickable or not an Element', + 'no such element', + ]); + }); + it('should throw for recursively hidden nodes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/button.html'); + using button = (await page.$('button'))!; + await page.evaluate((button: HTMLElement) => { + return (button.parentElement!.style.display = 'none'); + }, button); + const error = await button.click().catch(error_ => { + return error_; + }); + expect(error.message).atLeastOneToContain([ + 'Node is either not clickable or not an Element', + 'no such element', + ]); + }); + it('should throw for <br> elements', async () => { + const {page} = await getTestState(); + + await page.setContent('hello<br>goodbye'); + using br = (await page.$('br'))!; + const error = await br.click().catch(error_ => { + return error_; + }); + expect(error.message).atLeastOneToContain([ + 'Node is either not clickable or not an Element', + 'no such node', + ]); + }); + }); + + describe('ElementHandle.clickablePoint', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + document.body.style.padding = '0'; + document.body.style.margin = '0'; + document.body.innerHTML = ` + <div style="cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;"></div> + `; + }); + await page.evaluate(async () => { + return await new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + using divHandle = (await page.$('div'))!; + expect(await divHandle.clickablePoint()).toEqual({ + x: 45 + 60, // margin + middle point offset + y: 45 + 30, // margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 30 + 10, // margin + offset + y: 30 + 15, // margin + offset + }); + }); + + it('should not work if the click box is not visible', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<button style="width: 10px; height: 10px; position: absolute; left: -20px"></button>' + ); + using handle = await page.locator('button').waitHandle(); + await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error); + + await page.setContent( + '<button style="width: 10px; height: 10px; position: absolute; right: -20px"></button>' + ); + using handle2 = await page.locator('button').waitHandle(); + await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error); + + await page.setContent( + '<button style="width: 10px; height: 10px; position: absolute; top: -20px"></button>' + ); + using handle3 = await page.locator('button').waitHandle(); + await expect(handle3.clickablePoint()).rejects.toBeInstanceOf(Error); + + await page.setContent( + '<button style="width: 10px; height: 10px; position: absolute; bottom: -20px"></button>' + ); + using handle4 = await page.locator('button').waitHandle(); + await expect(handle4.clickablePoint()).rejects.toBeInstanceOf(Error); + }); + + it('should not work if the click box is not visible due to the iframe', async () => { + const {page} = await getTestState(); + + await page.setContent( + `<iframe name='frame' style='position: absolute; left: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>` + ); + const frame = await page.waitForFrame(frame => { + return frame.name() === 'frame'; + }); + + using handle = await frame.locator('button').waitHandle(); + await expect(handle.clickablePoint()).rejects.toBeInstanceOf(Error); + + await page.setContent( + `<iframe name='frame2' style='position: absolute; top: -100px' srcdoc="<button style='width: 10px; height: 10px;'></button>"></iframe>` + ); + const frame2 = await page.waitForFrame(frame => { + return frame.name() === 'frame2'; + }); + + using handle2 = await frame2.locator('button').waitHandle(); + await expect(handle2.clickablePoint()).rejects.toBeInstanceOf(Error); + }); + + it('should work for iframes', async () => { + const {page} = await getTestState(); + await page.evaluate(() => { + document.body.style.padding = '10px'; + document.body.style.margin = '10px'; + document.body.innerHTML = ` + <iframe style="border: none; margin: 0; padding: 0;" seamless sandbox srcdoc="<style>* { margin: 0; padding: 0;}</style><div style='cursor: pointer; width: 120px; height: 60px; margin: 30px; padding: 15px;' />"></iframe> + `; + }); + await page.evaluate(async () => { + return await new Promise(resolve => { + return window.requestAnimationFrame(resolve); + }); + }); + const frame = page.frames()[1]!; + using divHandle = (await frame.$('div'))!; + expect(await divHandle.clickablePoint()).toEqual({ + x: 20 + 45 + 60, // iframe pos + margin + middle point offset + y: 20 + 45 + 30, // iframe pos + margin + middle point offset + }); + expect( + await divHandle.clickablePoint({ + x: 10, + y: 15, + }) + ).toEqual({ + x: 20 + 30 + 10, // iframe pos + margin + offset + y: 20 + 30 + 15, // iframe pos + margin + offset + }); + }); + }); + + describe('Element.waitForSelector', () => { + it('should wait correctly with waitForSelector on an element', async () => { + const {page} = await getTestState(); + const waitFor = page.waitForSelector('.foo').catch(err => { + return err; + }) as Promise<ElementHandle<HTMLDivElement>>; + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>' + ); + using element = (await waitFor)!; + if (element instanceof Error) { + throw element; + } + expect(element).toBeDefined(); + + const innerWaitFor = element.waitForSelector('.bar').catch(err => { + return err; + }) as Promise<ElementHandle<HTMLDivElement>>; + await element.evaluate(el => { + el.innerHTML = '<div class="bar">bar1</div>'; + }); + using element2 = (await innerWaitFor)!; + if (element2 instanceof Error) { + throw element2; + } + expect(element2).toBeDefined(); + expect( + await element2.evaluate(el => { + return (el as HTMLElement).innerText; + }) + ).toStrictEqual('bar1'); + }); + }); + + describe('Element.waitForXPath', () => { + it('should wait correctly with waitForXPath on an element', async () => { + const {page} = await getTestState(); + // Set the page content after the waitFor has been started. + await page.setContent( + `<div id=el1> + el1 + <div id=el2> + el2 + </div> + </div> + <div id=el3> + el3 + </div>` + ); + + using el1 = (await page.waitForSelector( + '#el1' + )) as ElementHandle<HTMLDivElement>; + + for (const path of ['//div', './/div']) { + using e = (await el1.waitForXPath( + path + )) as ElementHandle<HTMLDivElement>; + expect( + await e.evaluate(el => { + return el.id; + }) + ).toStrictEqual('el2'); + } + }); + }); + + describe('ElementHandle.hover', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + using button = (await page.$('#button-6'))!; + await button.hover(); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-6'); + }); + }); + + describe('ElementHandle.isIntersectingViewport', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + async function getVisibilityForButton(selector: string) { + using button = (await page.$(selector))!; + return await button.isIntersectingViewport(); + } + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + const buttonsPromises = []; + // Firefox seems slow when using `isIntersectingViewport` + // so we do all the tasks asynchronously + for (let i = 0; i < 11; ++i) { + buttonsPromises.push(getVisibilityForButton('#btn' + i)); + } + const buttonVisibility = await Promise.all(buttonsPromises); + for (let i = 0; i < 11; ++i) { + // All but last button are visible. + const visible = i < 10; + expect(buttonVisibility[i]).toBe(visible); + } + }); + it('should work with threshold', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + using button = (await page.$('#btn11'))!; + expect( + await button.isIntersectingViewport({ + threshold: 0.001, + }) + ).toBe(false); + }); + it('should work with threshold of 1', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/offscreenbuttons.html'); + // a button almost cannot be seen + // sometimes we expect to return false by isIntersectingViewport1 + using button = (await page.$('#btn0'))!; + expect( + await button.isIntersectingViewport({ + threshold: 1, + }) + ).toBe(true); + }); + it('should work with svg elements', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/inline-svg.html'); + const [visibleCircle, visibleSvg] = await Promise.all([ + page.$('circle'), + page.$('svg'), + ]); + + // Firefox seems slow when using `isIntersectingViewport` + // so we do all the tasks asynchronously + const [ + circleThresholdOne, + circleThresholdZero, + svgThresholdOne, + svgThresholdZero, + ] = await Promise.all([ + visibleCircle!.isIntersectingViewport({ + threshold: 1, + }), + visibleCircle!.isIntersectingViewport({ + threshold: 0, + }), + visibleSvg!.isIntersectingViewport({ + threshold: 1, + }), + visibleSvg!.isIntersectingViewport({ + threshold: 0, + }), + ]); + + expect(circleThresholdOne).toBe(true); + expect(circleThresholdZero).toBe(true); + expect(svgThresholdOne).toBe(true); + expect(svgThresholdZero).toBe(true); + + const [invisibleCircle, invisibleSvg] = await Promise.all([ + page.$('div circle'), + page.$('div svg'), + ]); + + // Firefox seems slow when using `isIntersectingViewport` + // so we do all the tasks asynchronously + const [ + invisibleCircleThresholdOne, + invisibleCircleThresholdZero, + invisibleSvgThresholdOne, + invisibleSvgThresholdZero, + ] = await Promise.all([ + invisibleCircle!.isIntersectingViewport({ + threshold: 1, + }), + invisibleCircle!.isIntersectingViewport({ + threshold: 0, + }), + invisibleSvg!.isIntersectingViewport({ + threshold: 1, + }), + invisibleSvg!.isIntersectingViewport({ + threshold: 0, + }), + ]); + + expect(invisibleCircleThresholdOne).toBe(false); + expect(invisibleCircleThresholdZero).toBe(false); + expect(invisibleSvgThresholdOne).toBe(false); + expect(invisibleSvgThresholdZero).toBe(false); + }); + }); + + describe('Custom queries', function () { + afterEach(() => { + Puppeteer.clearCustomQueryHandlers(); + }); + it('should register and unregister', async () => { + const {page} = await getTestState(); + await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); + + // Register. + Puppeteer.registerCustomQueryHandler('getById', { + queryOne: (_element, selector) => { + return document.querySelector(`[id="${selector}"]`); + }, + }); + using element = (await page.$( + 'getById/foo' + )) as ElementHandle<HTMLDivElement>; + expect( + await page.evaluate(element => { + return element.id; + }, element) + ).toBe('foo'); + const handlerNamesAfterRegistering = Puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterRegistering.includes('getById')).toBeTruthy(); + + // Unregister. + Puppeteer.unregisterCustomQueryHandler('getById'); + try { + await page.$('getById/foo'); + throw new Error('Custom query handler name not set - throw expected'); + } catch (error) { + expect(error).not.toStrictEqual( + new Error('Custom query handler name not set - throw expected') + ); + } + const handlerNamesAfterUnregistering = + Puppeteer.customQueryHandlerNames(); + expect(handlerNamesAfterUnregistering.includes('getById')).toBeFalsy(); + }); + it('should throw with invalid query names', async () => { + try { + Puppeteer.registerCustomQueryHandler('1/2/3', { + queryOne: () => { + return document.querySelector('foo'); + }, + }); + throw new Error( + 'Custom query handler name was invalid - throw expected' + ); + } catch (error) { + expect(error).toStrictEqual( + new Error('Custom query handler names may only contain [a-zA-Z]') + ); + } + }); + it('should work for multiple elements', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (_element, selector) => { + return [...document.querySelectorAll(`.${selector}`)]; + }, + }); + const elements = (await page.$$('getByClass/foo')) as Array< + ElementHandle<HTMLDivElement> + >; + const classNames = await Promise.all( + elements.map(async element => { + return await page.evaluate(element => { + return element.className; + }, element); + }) + ); + + expect(classNames).toStrictEqual(['foo', 'foo baz']); + }); + it('should eval correctly', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryAll: (_element, selector) => { + return [...document.querySelectorAll(`.${selector}`)]; + }, + }); + const elements = await page.$$eval('getByClass/foo', divs => { + return divs.length; + }); + + expect(elements).toBe(2); + }); + it('should wait correctly with waitForSelector', async () => { + const {page} = await getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo').catch(err => { + return err; + }); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + if (element instanceof Error) { + throw element; + } + + expect(element).toBeDefined(); + }); + + it('should wait correctly with waitForSelector on an element', async () => { + const {page} = await getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo').catch(err => { + return err; + }) as Promise<ElementHandle<HTMLElement>>; + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="bar">bar2</div><div class="foo">Foo1</div>' + ); + using element = (await waitFor)!; + if (element instanceof Error) { + throw element; + } + expect(element).toBeDefined(); + + const innerWaitFor = element + .waitForSelector('getByClass/bar') + .catch(err => { + return err; + }) as Promise<ElementHandle<HTMLElement>>; + + await element.evaluate(el => { + el.innerHTML = '<div class="bar">bar1</div>'; + }); + + using element2 = (await innerWaitFor)!; + if (element2 instanceof Error) { + throw element2; + } + expect(element2).toBeDefined(); + expect( + await element2.evaluate(el => { + return el.innerText; + }) + ).toStrictEqual('bar1'); + }); + + it('should wait correctly with waitFor', async () => { + /* page.waitFor is deprecated so we silence the warning to avoid test noise */ + sinon.stub(console, 'warn').callsFake(() => {}); + const {page} = await getTestState(); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + }); + const waitFor = page.waitForSelector('getByClass/foo').catch(err => { + return err; + }); + + // Set the page content after the waitFor has been started. + await page.setContent( + '<div id="not-foo"></div><div class="foo">Foo1</div>' + ); + const element = await waitFor; + + if (element instanceof Error) { + throw element; + } + + expect(element).toBeDefined(); + }); + it('should work when both queryOne and queryAll are registered', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo"><div id="nested-foo" class="foo"/></div><div class="foo baz">Foo2</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(`.${selector}`)]; + }, + }); + + using element = (await page.$('getByClass/foo'))!; + expect(element).toBeDefined(); + + const elements = await page.$$('getByClass/foo'); + expect(elements).toHaveLength(3); + }); + it('should eval when both queryOne and queryAll are registered', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div id="not-foo"></div><div class="foo">text</div><div class="foo baz">content</div>' + ); + Puppeteer.registerCustomQueryHandler('getByClass', { + queryOne: (element, selector) => { + return (element as Element).querySelector(`.${selector}`); + }, + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(`.${selector}`)]; + }, + }); + + const txtContent = await page.$eval('getByClass/foo', div => { + return div.textContent; + }); + expect(txtContent).toBe('text'); + + const txtContents = await page.$$eval('getByClass/foo', divs => { + return divs + .map(d => { + return d.textContent; + }) + .join(''); + }); + expect(txtContents).toBe('textcontent'); + }); + + it('should work with function shorthands', async () => { + const {page} = await getTestState(); + await page.setContent('<div id="not-foo"></div><div id="foo"></div>'); + + Puppeteer.registerCustomQueryHandler('getById', { + // This is a function shorthand + queryOne(_element, selector) { + return document.querySelector(`[id="${selector}"]`); + }, + }); + + using element = (await page.$( + 'getById/foo' + )) as ElementHandle<HTMLDivElement>; + expect( + await page.evaluate(element => { + return element.id; + }, element) + ).toBe('foo'); + }); + }); + + describe('ElementHandle.toElement', () => { + it('should work', async () => { + const {page} = await getTestState(); + await page.setContent('<div class="foo">Foo1</div>'); + using element = await page.$('.foo'); + using div = await element?.toElement('div'); + expect(div).toBeDefined(); + }); + }); + + describe('ElementHandle[Symbol.dispose]', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('document'); + const spy = sinon.spy(handle, disposeSymbol); + { + using _ = handle; + } + expect(handle).toBeInstanceOf(ElementHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeTruthy(); + }); + }); + + describe('ElementHandle[Symbol.asyncDispose]', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('document'); + const spy = sinon.spy(handle, asyncDisposeSymbol); + { + await using _ = handle; + } + expect(handle).toBeInstanceOf(ElementHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeTruthy(); + }); + }); + + describe('ElementHandle.move', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('document'); + const spy = sinon.spy(handle, disposeSymbol); + { + using _ = handle; + handle.move(); + } + expect(handle).toBeInstanceOf(ElementHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeFalsy(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/emulation.spec.ts b/remote/test/puppeteer/test/src/emulation.spec.ts new file mode 100644 index 0000000000..823061c450 --- /dev/null +++ b/remote/test/puppeteer/test/src/emulation.spec.ts @@ -0,0 +1,553 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {KnownDevices, PredefinedNetworkConditions} from 'puppeteer'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +const iPhone = KnownDevices['iPhone 6']; +const iPhoneLandscape = KnownDevices['iPhone 6 landscape']; + +describe('Emulation', () => { + setupTestBrowserHooks(); + + describe('Page.viewport', function () { + it('should get the proper viewport size', async () => { + const {page} = await getTestState(); + + expect(page.viewport()).toEqual({width: 800, height: 600}); + await page.setViewport({width: 123, height: 456}); + expect(page.viewport()).toEqual({width: 123, height: 456}); + }); + it('should support mobile emulation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return window.innerWidth; + }) + ).toBe(800); + await page.setViewport(iPhone.viewport); + expect( + await page.evaluate(() => { + return window.innerWidth; + }) + ).toBe(375); + await page.setViewport({width: 400, height: 300}); + expect( + await page.evaluate(() => { + return window.innerWidth; + }) + ).toBe(400); + }); + it('should support touch emulation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return 'ontouchstart' in window; + }) + ).toBe(false); + await page.setViewport(iPhone.viewport); + expect( + await page.evaluate(() => { + return 'ontouchstart' in window; + }) + ).toBe(true); + expect(await page.evaluate(dispatchTouch)).toBe('Received touch'); + await page.setViewport({width: 100, height: 100}); + expect( + await page.evaluate(() => { + return 'ontouchstart' in window; + }) + ).toBe(false); + + function dispatchTouch() { + let fulfill!: (value: string) => void; + const promise = new Promise(x => { + fulfill = x; + }); + window.ontouchstart = () => { + fulfill('Received touch'); + }; + window.dispatchEvent(new Event('touchstart')); + + fulfill('Did not receive touch'); + + return promise; + } + }); + it('should be detectable by Modernizr', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/detect-touch.html'); + expect( + await page.evaluate(() => { + return document.body.textContent!.trim(); + }) + ).toBe('NO'); + await page.setViewport(iPhone.viewport); + await page.goto(server.PREFIX + '/detect-touch.html'); + expect( + await page.evaluate(() => { + return document.body.textContent!.trim(); + }) + ).toBe('YES'); + }); + it('should detect touch when applying viewport with touches', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 800, height: 600, hasTouch: true}); + await page.addScriptTag({url: server.PREFIX + '/modernizr.js'}); + expect( + await page.evaluate(() => { + return (globalThis as any).Modernizr.touchevents; + }) + ).toBe(true); + }); + it('should support landscape emulation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return screen.orientation.type; + }) + ).toBe('portrait-primary'); + await page.setViewport(iPhoneLandscape.viewport); + expect( + await page.evaluate(() => { + return screen.orientation.type; + }) + ).toBe('landscape-primary'); + await page.setViewport({width: 100, height: 100}); + expect( + await page.evaluate(() => { + return screen.orientation.type; + }) + ).toBe('portrait-primary'); + }); + it('should update media queries when resoltion changes', async () => { + const {page, server} = await getTestState(); + + async function getFontSize() { + return await page.evaluate(() => { + return parseInt( + window.getComputedStyle(document.querySelector('p')!).fontSize, + 10 + ); + }); + } + + for (const dpr of [1, 2, 3]) { + await page.setViewport({ + width: 800, + height: 600, + deviceScaleFactor: dpr, + }); + + await page.goto(server.PREFIX + '/resolution.html'); + + await expect(getFontSize()).resolves.toEqual(dpr); + + const screenshot = await page.screenshot({ + fullPage: false, + }); + expect(screenshot).toBeGolden(`device-pixel-ratio${dpr}.png`); + } + }); + it('should load correct pictures when emulation dpr', async () => { + const {page, server} = await getTestState(); + + async function getCurrentSrc() { + return await page.evaluate(() => { + return document.querySelector('img')!.currentSrc; + }); + } + + for (const dpr of [1, 2, 3]) { + await page.setViewport({ + width: 800, + height: 600, + deviceScaleFactor: dpr, + }); + + await page.goto(server.PREFIX + '/picture.html'); + + await expect(getCurrentSrc()).resolves.toMatch( + new RegExp(`logo-${dpr}x.png`) + ); + } + }); + }); + + describe('Page.emulate', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + await page.emulate(iPhone); + expect( + await page.evaluate(() => { + return window.innerWidth; + }) + ).toBe(375); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('iPhone'); + }); + it('should support clicking', async () => { + const {page, server} = await getTestState(); + + await page.emulate(iPhone); + await page.goto(server.PREFIX + '/input/button.html'); + using button = (await page.$('button'))!; + await page.evaluate((button: HTMLElement) => { + return (button.style.marginTop = '200px'); + }, button); + await button.click(); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe('Clicked'); + }); + }); + + describe('Page.emulateMediaType', function () { + it('should work', async () => { + const {page} = await getTestState(); + + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(false); + await page.emulateMediaType('print'); + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(true); + await page.emulateMediaType(); + expect( + await page.evaluate(() => { + return matchMedia('screen').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('print').matches; + }) + ).toBe(false); + }); + it('should throw in case of bad argument', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page.emulateMediaType('bad').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported media type: bad'); + }); + }); + + describe('Page.emulateMediaFeatures', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.emulateMediaFeatures([ + {name: 'prefers-reduced-motion', value: 'reduce'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: reduce)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: no-preference)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'prefers-color-scheme', value: 'light'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: light)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'prefers-color-scheme', value: 'dark'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: light)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'prefers-reduced-motion', value: 'reduce'}, + {name: 'prefers-color-scheme', value: 'light'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: reduce)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-reduced-motion: no-preference)').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: light)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(prefers-color-scheme: dark)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([{name: 'color-gamut', value: 'srgb'}]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(false); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([{name: 'color-gamut', value: 'p3'}]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(false); + await page.emulateMediaFeatures([ + {name: 'color-gamut', value: 'rec2020'}, + ]); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: p3)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: srgb)').matches; + }) + ).toBe(true); + expect( + await page.evaluate(() => { + return matchMedia('(color-gamut: rec2020)').matches; + }) + ).toBe(true); + }); + it('should throw in case of bad argument', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .emulateMediaFeatures([{name: 'bad', value: ''}]) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported media feature: bad'); + }); + }); + + describe('Page.emulateTimezone', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + (globalThis as any).date = new Date(1479579154987); + }); + await page.emulateTimezone('America/Jamaica'); + expect( + await page.evaluate(() => { + return (globalThis as any).date.toString(); + }) + ).toBe('Sat Nov 19 2016 13:12:34 GMT-0500 (Eastern Standard Time)'); + + await page.emulateTimezone('Pacific/Honolulu'); + expect( + await page.evaluate(() => { + return (globalThis as any).date.toString(); + }) + ).toBe( + 'Sat Nov 19 2016 08:12:34 GMT-1000 (Hawaii-Aleutian Standard Time)' + ); + + await page.emulateTimezone('America/Buenos_Aires'); + expect( + await page.evaluate(() => { + return (globalThis as any).date.toString(); + }) + ).toBe('Sat Nov 19 2016 15:12:34 GMT-0300 (Argentina Standard Time)'); + + await page.emulateTimezone('Europe/Berlin'); + expect( + await page.evaluate(() => { + return (globalThis as any).date.toString(); + }) + ).toBe( + 'Sat Nov 19 2016 19:12:34 GMT+0100 (Central European Standard Time)' + ); + }); + + it('should throw for invalid timezone IDs', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page.emulateTimezone('Foo/Bar').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Invalid timezone ID: Foo/Bar'); + await page.emulateTimezone('Baz/Qux').catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Invalid timezone ID: Baz/Qux'); + }); + }); + + describe('Page.emulateVisionDeficiency', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + + { + await page.emulateVisionDeficiency('achromatopsia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-achromatopsia.png'); + } + + { + await page.emulateVisionDeficiency('blurredVision'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-blurredVision.png'); + } + + { + await page.emulateVisionDeficiency('deuteranopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-deuteranopia.png'); + } + + { + await page.emulateVisionDeficiency('protanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-protanopia.png'); + } + + { + await page.emulateVisionDeficiency('tritanopia'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('vision-deficiency-tritanopia.png'); + } + + { + await page.emulateVisionDeficiency('none'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + } + }); + + it('should throw for invalid vision deficiencies', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + // @ts-expect-error deliberately passing invalid deficiency + .emulateVisionDeficiency('invalid') + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe('Unsupported vision deficiency: invalid'); + }); + }); + + describe('Page.emulateNetworkConditions', function () { + it('should change navigator.connection.effectiveType', async () => { + const {page} = await getTestState(); + + const slow3G = PredefinedNetworkConditions['Slow 3G']!; + const fast3G = PredefinedNetworkConditions['Fast 3G']!; + + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('4g'); + await page.emulateNetworkConditions(fast3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('3g'); + await page.emulateNetworkConditions(slow3G); + expect( + await page.evaluate('window.navigator.connection.effectiveType') + ).toBe('2g'); + await page.emulateNetworkConditions(null); + }); + }); + + describe('Page.emulateCPUThrottling', function () { + it('should change the CPU throttling rate successfully', async () => { + const {page} = await getTestState(); + + await page.emulateCPUThrottling(100); + await page.emulateCPUThrottling(null); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/evaluation.spec.ts b/remote/test/puppeteer/test/src/evaluation.spec.ts new file mode 100644 index 0000000000..3305b59cc2 --- /dev/null +++ b/remote/test/puppeteer/test/src/evaluation.spec.ts @@ -0,0 +1,607 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Evaluation specs', function () { + setupTestBrowserHooks(); + + describe('Page.evaluate', function () { + it('should work', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return 7 * 3; + }); + expect(result).toBe(21); + }); + it('should transfer BigInt', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate((a: bigint) => { + return a; + }, BigInt(42)); + expect(result).toBe(BigInt(42)); + }); + it('should transfer NaN', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(a => { + return a; + }, NaN); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should transfer -0', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -0); + expect(Object.is(result, -0)).toBe(true); + }); + it('should transfer Infinity', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(a => { + return a; + }, Infinity); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should transfer -Infinity', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(a => { + return a; + }, -Infinity); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should transfer arrays', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate( + a => { + return a; + }, + [1, 2, 3] + ); + expect(result).toEqual([1, 2, 3]); + }); + it('should transfer arrays as arrays, not objects', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate( + a => { + return Array.isArray(a); + }, + [1, 2, 3] + ); + expect(result).toBe(true); + }); + it('should modify global environment', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + return ((globalThis as any).globalVar = 123); + }); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should evaluate in the page context', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/global-var.html'); + expect(await page.evaluate('globalVar')).toBe(123); + }); + it('should replace symbols with undefined', async () => { + const {page} = await getTestState(); + + expect( + await page.evaluate(() => { + return [Symbol('foo4'), 'foo']; + }) + ).toEqual([undefined, 'foo']); + }); + it('should work with function shorthands', async () => { + const {page} = await getTestState(); + + const a = { + sum(a: number, b: number) { + return a + b; + }, + + async mult(a: number, b: number) { + return a * b; + }, + }; + expect(await page.evaluate(a.sum, 1, 2)).toBe(3); + expect(await page.evaluate(a.mult, 2, 4)).toBe(8); + }); + it('should work with unicode chars', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate( + a => { + return a['中文字符']; + }, + { + 中文字符: 42, + } + ); + expect(result).toBe(42); + }); + it('should throw when evaluation triggers reload', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .evaluate(() => { + location.reload(); + return new Promise(() => {}); + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Protocol error'); + }); + it('should await promise', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return Promise.resolve(8 * 7); + }); + expect(result).toBe(56); + }); + it('should work right after framenavigated', async () => { + const {page, server} = await getTestState(); + + let frameEvaluation = null; + page.on('framenavigated', async frame => { + frameEvaluation = frame.evaluate(() => { + return 6 * 7; + }); + }); + await page.goto(server.EMPTY_PAGE); + expect(await frameEvaluation).toBe(42); + }); + it('should work from-inside an exposed function', async () => { + const {page} = await getTestState(); + + // Setup inpage callback, which calls Page.evaluate + await page.exposeFunction( + 'callController', + async function (a: number, b: number) { + return await page.evaluate( + (a: number, b: number): number => { + return a * b; + }, + a, + b + ); + } + ); + const result = await page.evaluate(async function () { + return (globalThis as any).callController(9, 3); + }); + expect(result).toBe(27); + }); + it('should reject promise with exception', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .evaluate(() => { + // @ts-expect-error we know the object doesn't exist + return notExistingObject.property; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain('notExistingObject'); + }); + it('should support thrown strings as error messages', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 'qwerty'; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toEqual('qwerty'); + }); + it('should support thrown numbers as error messages', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .evaluate(() => { + throw 100500; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toEqual(100500); + }); + it('should return complex objects', async () => { + const {page} = await getTestState(); + + const object = {foo: 'bar!'}; + const result = await page.evaluate(a => { + return a; + }, object); + expect(result).not.toBe(object); + expect(result).toEqual(object); + }); + it('should return BigInt', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return BigInt(42); + }); + expect(result).toBe(BigInt(42)); + }); + it('should return NaN', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return NaN; + }); + expect(Object.is(result, NaN)).toBe(true); + }); + it('should return -0', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return -0; + }); + expect(Object.is(result, -0)).toBe(true); + }); + it('should return Infinity', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return Infinity; + }); + expect(Object.is(result, Infinity)).toBe(true); + }); + it('should return -Infinity', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return -Infinity; + }); + expect(Object.is(result, -Infinity)).toBe(true); + }); + it('should accept "null" as one of multiple parameters', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate( + (a, b) => { + return Object.is(a, null) && Object.is(b, 'foo'); + }, + null, + 'foo' + ); + expect(result).toBe(true); + }); + it('should properly serialize null fields', async () => { + const {page} = await getTestState(); + + expect( + await page.evaluate(() => { + return {a: undefined}; + }) + ).toEqual({}); + }); + it('should return undefined for non-serializable objects', async () => { + const {page} = await getTestState(); + + expect( + await page.evaluate(() => { + return window; + }) + ).toBe(undefined); + }); + it('should return promise as empty object', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + return { + promise: new Promise(resolve => { + setTimeout(resolve, 1000); + }), + }; + }); + expect(result).toEqual({ + promise: {}, + }); + }); + it('should work for circular object', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + const a: Record<string, unknown> = { + c: 5, + d: { + foo: 'bar', + }, + }; + const b = {a}; + a['b'] = b; + return a; + }); + expect(result).toMatchObject({ + c: 5, + d: { + foo: 'bar', + }, + b: { + a: undefined, + }, + }); + }); + it('should accept a string', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate('1 + 2'); + expect(result).toBe(3); + }); + it('should accept a string with semi colons', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate('1 + 5;'); + expect(result).toBe(6); + }); + it('should accept a string with comments', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate('2 + 5;\n// do some math!'); + expect(result).toBe(7); + }); + it('should accept element handle as an argument', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>42</section>'); + using element = (await page.$('section'))!; + const text = await page.evaluate(e => { + return e.textContent; + }, element); + expect(text).toBe('42'); + }); + it('should throw if underlying element was disposed', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>39</section>'); + using element = (await page.$('section'))!; + expect(element).toBeTruthy(); + // We want to dispose early. + await element.dispose(); + let error!: Error; + await page + .evaluate((e: HTMLElement) => { + return e.textContent; + }, element) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('JSHandle is disposed'); + }); + it('should throw if elementHandles are from other frames', async () => { + const {page, server} = await getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + using bodyHandle = await page.frames()[1]!.$('body'); + let error!: Error; + await page + .evaluate(body => { + return body?.innerHTML; + }, bodyHandle) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + expect(error.message).toContain( + 'JSHandles can be evaluated only in the context they were created' + ); + }); + it('should simulate a user gesture', async () => { + const {page} = await getTestState(); + + const result = await page.evaluate(() => { + document.body.appendChild(document.createTextNode('test')); + document.execCommand('selectAll'); + return document.execCommand('copy'); + }); + expect(result).toBe(true); + }); + it('should not throw an error when evaluation does a navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/one-style.html'); + const onRequest = server.waitForRequest('/empty.html'); + const result = await page.evaluate(() => { + (window as any).location = '/empty.html'; + return [42]; + }); + expect(result).toEqual([42]); + await onRequest; + }); + it('should transfer 100Mb of data from page to node.js', async function () { + this.timeout(25_000); + const {page} = await getTestState(); + + const a = await page.evaluate(() => { + return Array(100 * 1024 * 1024 + 1).join('a'); + }); + expect(a.length).toBe(100 * 1024 * 1024); + }); + it('should throw error with detailed information on exception inside promise', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .evaluate(() => { + return new Promise(() => { + throw new Error('Error in promise'); + }); + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Error in promise'); + }); + + it('should return properly serialize objects with unknown type fields', async () => { + const {page} = await getTestState(); + await page.setContent( + "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='>" + ); + + const result = await page.evaluate(async () => { + const image = document.querySelector('img')!; + const imageBitmap = await createImageBitmap(image); + + return { + a: 'foo', + b: imageBitmap, + }; + }); + + expect(result).toEqual({ + a: 'foo', + b: undefined, + }); + }); + }); + + describe('Page.evaluateOnNewDocument', function () { + it('should evaluate before anything else on the page', async () => { + const {page, server} = await getTestState(); + + await page.evaluateOnNewDocument(function () { + (globalThis as any).injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe(123); + }); + it('should work with CSP', async () => { + const {page, server} = await getTestState(); + + server.setCSP('/empty.html', 'script-src ' + server.PREFIX); + await page.evaluateOnNewDocument(function () { + (globalThis as any).injected = 123; + }); + await page.goto(server.PREFIX + '/empty.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).injected; + }) + ).toBe(123); + + // Make sure CSP works. + await page.addScriptTag({content: 'window.e = 10;'}).catch(error => { + return void error; + }); + expect( + await page.evaluate(() => { + return (window as any).e; + }) + ).toBe(undefined); + }); + }); + + describe('Page.removeScriptToEvaluateOnNewDocument', function () { + it('should remove new document script', async () => { + const {page, server} = await getTestState(); + + const {identifier} = await page.evaluateOnNewDocument(function () { + (globalThis as any).injected = 123; + }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect( + await page.evaluate(() => { + return (globalThis as any).result; + }) + ).toBe(123); + + await page.removeScriptToEvaluateOnNewDocument(identifier); + await page.reload(); + expect( + await page.evaluate(() => { + return (globalThis as any).result || null; + }) + ).toBe(null); + }); + }); + + describe('Frame.evaluate', function () { + it('should have different execution contexts', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(2); + await page.frames()[0]!.evaluate(() => { + return ((globalThis as any).FOO = 'foo'); + }); + await page.frames()[1]!.evaluate(() => { + return ((globalThis as any).FOO = 'bar'); + }); + expect( + await page.frames()[0]!.evaluate(() => { + return (globalThis as any).FOO; + }) + ).toBe('foo'); + expect( + await page.frames()[1]!.evaluate(() => { + return (globalThis as any).FOO; + }) + ).toBe('bar'); + }); + it('should have correct execution contexts', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames()).toHaveLength(2); + expect( + await page.frames()[0]!.evaluate(() => { + return document.body.textContent!.trim(); + }) + ).toBe(''); + expect( + await page.frames()[1]!.evaluate(() => { + return document.body.textContent!.trim(); + }) + ).toBe(`Hi, I'm frame`); + }); + it('should execute after cross-site navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect( + await mainFrame.evaluate(() => { + return window.location.href; + }) + ).toContain('localhost'); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect( + await mainFrame.evaluate(() => { + return window.location.href; + }) + ).toContain('127'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/fixtures.spec.ts b/remote/test/puppeteer/test/src/fixtures.spec.ts new file mode 100644 index 0000000000..ca11e94cac --- /dev/null +++ b/remote/test/puppeteer/test/src/fixtures.spec.ts @@ -0,0 +1,114 @@ +/** + * @license + * Copyright 2019 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {spawn, execSync} from 'child_process'; +import path from 'path'; + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Fixtures', function () { + setupTestBrowserHooks(); + + it('dumpio option should work with pipe option', async () => { + const {defaultBrowserOptions, puppeteerPath, headless} = + await getTestState(); + if (headless !== 'true') { + // This test only works in the old headless mode. + return; + } + + let dumpioData = ''; + const options = Object.assign({}, defaultBrowserOptions, { + pipe: true, + dumpio: true, + }); + const res = spawn('node', [ + path.join(__dirname, '../fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', data => { + dumpioData += data.toString('utf8'); + }); + await new Promise(resolve => { + return res.on('close', resolve); + }); + expect(dumpioData).toContain('message from dumpio'); + }); + it('should dump browser process stderr', async () => { + const {defaultBrowserOptions, puppeteerPath} = await getTestState(); + + let dumpioData = ''; + const options = Object.assign({}, defaultBrowserOptions, {dumpio: true}); + const res = spawn('node', [ + path.join(__dirname, '../fixtures', 'dumpio.js'), + puppeteerPath, + JSON.stringify(options), + ]); + res.stderr.on('data', data => { + dumpioData += data.toString('utf8'); + }); + await new Promise(resolve => { + return res.on('close', resolve); + }); + expect(dumpioData).toContain('DevTools listening on ws://'); + }); + it('should close the browser when the node process closes', async () => { + const {defaultBrowserOptions, puppeteerPath, puppeteer} = + await getTestState(); + + const options = Object.assign({}, defaultBrowserOptions, { + // Disable DUMPIO to cleanly read stdout. + dumpio: false, + }); + const res = spawn('node', [ + path.join(__dirname, '../fixtures', 'closeme.js'), + puppeteerPath, + JSON.stringify(options), + ]); + let killed = false; + function killProcess() { + if (killed) { + return; + } + if (process.platform === 'win32') { + execSync(`taskkill /pid ${res.pid} /T /F`); + } else { + process.kill(res.pid!); + } + killed = true; + } + try { + let wsEndPointCallback: (value: string) => void; + const wsEndPointPromise = new Promise<string>(x => { + wsEndPointCallback = x; + }); + let output = ''; + res.stdout.on('data', data => { + output += data; + if (output.indexOf('\n')) { + wsEndPointCallback(output.substring(0, output.indexOf('\n'))); + } + }); + const browser = await puppeteer.connect({ + browserWSEndpoint: await wsEndPointPromise, + }); + const promises = [ + waitEvent(browser, 'disconnected'), + new Promise(resolve => { + res.on('close', resolve); + }), + ]; + killProcess(); + await Promise.all(promises); + } finally { + killProcess(); + } + }); +}); diff --git a/remote/test/puppeteer/test/src/frame.spec.ts b/remote/test/puppeteer/test/src/frame.spec.ts new file mode 100644 index 0000000000..3b2456821a --- /dev/null +++ b/remote/test/puppeteer/test/src/frame.spec.ts @@ -0,0 +1,297 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js'; +import type {Frame} from 'puppeteer-core/internal/api/Frame.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import { + attachFrame, + detachFrame, + dumpFrames, + navigateFrame, + waitEvent, +} from './utils.js'; + +describe('Frame specs', function () { + setupTestBrowserHooks(); + + describe('Frame.evaluateHandle', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + using windowHandle = await mainFrame.evaluateHandle(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + }); + + describe('Frame.evaluate', function () { + it('should throw for detached frames', async () => { + const {page, server} = await getTestState(); + + const frame1 = (await attachFrame(page, 'frame1', server.EMPTY_PAGE))!; + await detachFrame(page, 'frame1'); + let error: Error | undefined; + try { + await frame1.evaluate(() => { + return 7 * 8; + }); + } catch (err) { + error = err as Error; + } + expect(error?.message).toContain('Attempted to use detached Frame'); + }); + + it('allows readonly array to be an argument', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + + // This test checks if Frame.evaluate allows a readonly array to be an argument. + // See https://github.com/puppeteer/puppeteer/issues/6953. + const readonlyArray: readonly string[] = ['a', 'b', 'c']; + await mainFrame.evaluate(arr => { + return arr; + }, readonlyArray); + }); + }); + + describe('Frame.page', function () { + it('should retrieve the page from a frame', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + expect(mainFrame.page()).toEqual(page); + }); + }); + + describe('Frame Management', function () { + it('should handle nested frames', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + expect(dumpFrames(page.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + }); + it('should send events when frames are manipulated dynamically', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + // validate frameattached events + const attachedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + await attachFrame(page, 'frame1', './assets/frame.html'); + expect(attachedFrames).toHaveLength(1); + expect(attachedFrames[0]!.url()).toContain('/assets/frame.html'); + + // validate framenavigated events + const navigatedFrames: Frame[] = []; + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await navigateFrame(page, 'frame1', './empty.html'); + expect(navigatedFrames).toHaveLength(1); + expect(navigatedFrames[0]!.url()).toBe(server.EMPTY_PAGE); + + // validate framedetached events + const detachedFrames: Frame[] = []; + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + await detachFrame(page, 'frame1'); + expect(detachedFrames).toHaveLength(1); + expect(detachedFrames[0]!.isDetached()).toBe(true); + }); + it('should send "framenavigated" when navigating on anchor URLs', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.goto(server.EMPTY_PAGE + '#foo'), + waitEvent(page, 'framenavigated'), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + }); + it('should persist mainFrame on cross-process navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const mainFrame = page.mainFrame(); + await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + expect(page.mainFrame() === mainFrame).toBeTruthy(); + }); + it('should not send attach/detach events for main frame', async () => { + const {page, server} = await getTestState(); + + let hasEvents = false; + page.on('frameattached', () => { + return (hasEvents = true); + }); + page.on('framedetached', () => { + return (hasEvents = true); + }); + await page.goto(server.EMPTY_PAGE); + expect(hasEvents).toBe(false); + }); + it('should detach child frames on navigation', async () => { + const {page, server} = await getTestState(); + + let attachedFrames: Frame[] = []; + let detachedFrames: Frame[] = []; + let navigatedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + + expect(attachedFrames).toHaveLength(4); + expect(detachedFrames).toHaveLength(0); + expect(navigatedFrames).toHaveLength(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames).toHaveLength(0); + expect(detachedFrames).toHaveLength(4); + expect(navigatedFrames).toHaveLength(1); + }); + it('should support framesets', async () => { + const {page, server} = await getTestState(); + + let attachedFrames: Frame[] = []; + let detachedFrames: Frame[] = []; + let navigatedFrames: Frame[] = []; + page.on('frameattached', frame => { + return attachedFrames.push(frame); + }); + page.on('framedetached', frame => { + return detachedFrames.push(frame); + }); + page.on('framenavigated', frame => { + return navigatedFrames.push(frame); + }); + await page.goto(server.PREFIX + '/frames/frameset.html'); + expect(attachedFrames).toHaveLength(4); + expect(detachedFrames).toHaveLength(0); + expect(navigatedFrames).toHaveLength(5); + + attachedFrames = []; + detachedFrames = []; + navigatedFrames = []; + await page.goto(server.EMPTY_PAGE); + expect(attachedFrames).toHaveLength(0); + expect(detachedFrames).toHaveLength(4); + expect(navigatedFrames).toHaveLength(1); + }); + it('should report frame from-inside shadow DOM', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/shadow.html'); + await page.evaluate(async (url: string) => { + const frame = document.createElement('iframe'); + frame.src = url; + document.body.shadowRoot!.appendChild(frame); + await new Promise(x => { + return (frame.onload = x); + }); + }, server.EMPTY_PAGE); + expect(page.frames()).toHaveLength(2); + expect(page.frames()[1]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should report frame.name()', async () => { + const {page, server} = await getTestState(); + + await attachFrame(page, 'theFrameId', server.EMPTY_PAGE); + await page.evaluate((url: string) => { + const frame = document.createElement('iframe'); + frame.name = 'theFrameName'; + frame.src = url; + document.body.appendChild(frame); + return new Promise(x => { + return (frame.onload = x); + }); + }, server.EMPTY_PAGE); + expect(page.frames()[0]!.name()).toBe(''); + expect(page.frames()[1]!.name()).toBe('theFrameId'); + expect(page.frames()[2]!.name()).toBe('theFrameName'); + }); + it('should report frame.parent()', async () => { + const {page, server} = await getTestState(); + + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await attachFrame(page, 'frame2', server.EMPTY_PAGE); + expect(page.frames()[0]!.parentFrame()).toBe(null); + expect(page.frames()[1]!.parentFrame()).toBe(page.mainFrame()); + expect(page.frames()[2]!.parentFrame()).toBe(page.mainFrame()); + }); + it('should report different frame instance when frame re-attaches', async () => { + const {page, server} = await getTestState(); + + const frame1 = await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.evaluate(() => { + (globalThis as any).frame = document.querySelector('#frame1'); + (globalThis as any).frame.remove(); + }); + expect(frame1!.isDetached()).toBe(true); + const [frame2] = await Promise.all([ + waitEvent(page, 'frameattached'), + page.evaluate(() => { + return document.body.appendChild((globalThis as any).frame); + }), + ]); + expect(frame2.isDetached()).toBe(false); + expect(frame1).not.toBe(frame2); + }); + it('should support url fragment', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame-url-fragment.html'); + + expect(page.frames()).toHaveLength(2); + expect(page.frames()[1]!.url()).toBe( + server.PREFIX + '/frames/frame.html?param=value#fragment' + ); + }); + it('should support lazy frames', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 1000, height: 1000}); + await page.goto(server.PREFIX + '/frames/lazy-frame.html'); + + expect( + page.frames().map(frame => { + return frame._hasStartedLoading; + }) + ).toEqual([true, true, false]); + }); + }); + + describe('Frame.client', function () { + it('should return the client instance', async () => { + const {page} = await getTestState(); + expect(page.mainFrame().client).toBeInstanceOf(CDPSession); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/golden-utils.ts b/remote/test/puppeteer/test/src/golden-utils.ts new file mode 100644 index 0000000000..939f69c968 --- /dev/null +++ b/remote/test/puppeteer/test/src/golden-utils.ts @@ -0,0 +1,169 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert'; +import fs from 'fs'; +import path from 'path'; + +import {diffLines} from 'diff'; +import jpeg from 'jpeg-js'; +import mime from 'mime'; +import pixelmatch from 'pixelmatch'; +import {PNG} from 'pngjs'; + +interface DiffFile { + diff: string | Buffer; + ext?: string; +} + +const GoldenComparators = new Map< + string, + ( + actualBuffer: string | Buffer, + expectedBuffer: string | Buffer, + mimeType: string + ) => DiffFile | undefined +>(); + +const addSuffix = ( + filePath: string, + suffix: string, + customExtension?: string +): string => { + const dirname = path.dirname(filePath); + const ext = path.extname(filePath); + const name = path.basename(filePath, ext); + return path.join(dirname, name + suffix + (customExtension || ext)); +}; + +const compareImages = ( + actualBuffer: string | Buffer, + expectedBuffer: string | Buffer, + mimeType: string +): DiffFile | undefined => { + assert(typeof actualBuffer !== 'string'); + assert(typeof expectedBuffer !== 'string'); + + const actual = + mimeType === 'image/png' + ? PNG.sync.read(actualBuffer) + : jpeg.decode(actualBuffer); + + const expected = + mimeType === 'image/png' + ? PNG.sync.read(expectedBuffer) + : jpeg.decode(expectedBuffer); + if (expected.width !== actual.width || expected.height !== actual.height) { + throw new Error( + `Sizes differ: expected image ${expected.width}px X ${expected.height}px, but got ${actual.width}px X ${actual.height}px.` + ); + } + const diff = new PNG({width: expected.width, height: expected.height}); + const count = pixelmatch( + expected.data, + actual.data, + diff.data, + expected.width, + expected.height, + {threshold: 0.1} + ); + return count > 0 ? {diff: PNG.sync.write(diff)} : undefined; +}; + +const compareText = ( + actual: string | Buffer, + expectedBuffer: string | Buffer +): DiffFile | undefined => { + assert(typeof actual === 'string'); + const expected = expectedBuffer.toString('utf-8'); + if (expected === actual) { + return; + } + const result = diffLines(expected, actual); + const html = result.reduce( + (text, change) => { + text += change.added + ? `<span class='ins'>${change.value}</span>` + : change.removed + ? `<span class='del'>${change.value}</span>` + : change.value; + return text; + }, + `<link rel="stylesheet" href="file://${path.join( + __dirname, + 'diffstyle.css' + )}">` + ); + return { + diff: html, + ext: '.html', + }; +}; + +GoldenComparators.set('image/png', compareImages); +GoldenComparators.set('image/jpeg', compareImages); +GoldenComparators.set('text/plain', compareText); + +export const compare = ( + goldenPath: string, + outputPath: string, + actual: string | Buffer, + goldenName: string +): {pass: true} | {pass: false; message: string} => { + goldenPath = path.normalize(goldenPath); + outputPath = path.normalize(outputPath); + const expectedPath = path.join(goldenPath, goldenName); + const actualPath = path.join(outputPath, goldenName); + + const messageSuffix = `Output is saved in "${path.basename( + outputPath + '" directory' + )}`; + + if (!fs.existsSync(expectedPath)) { + ensureOutputDir(); + fs.writeFileSync(actualPath, actual); + return { + pass: false, + message: `${goldenName} is missing in golden results. ${messageSuffix}`, + }; + } + const expected = fs.readFileSync(expectedPath); + const mimeType = mime.getType(goldenName); + assert(mimeType); + const comparator = GoldenComparators.get(mimeType); + if (!comparator) { + return { + pass: false, + message: `Failed to find comparator with type ${mimeType}: ${goldenName}`, + }; + } + const result = comparator(actual, expected, mimeType); + if (!result) { + return {pass: true}; + } + ensureOutputDir(); + if (goldenPath === outputPath) { + fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); + } else { + fs.writeFileSync(actualPath, actual); + // Copy expected to the output/ folder for convenience. + fs.writeFileSync(addSuffix(actualPath, '-expected'), expected); + } + if (result) { + const diffPath = addSuffix(actualPath, '-diff', result.ext); + fs.writeFileSync(diffPath, result.diff); + } + + return { + pass: false, + message: `${goldenName} mismatch! ${messageSuffix}`, + }; + + function ensureOutputDir() { + if (!fs.existsSync(outputPath)) { + fs.mkdirSync(outputPath); + } + } +}; diff --git a/remote/test/puppeteer/test/src/headful.spec.ts b/remote/test/puppeteer/test/src/headful.spec.ts new file mode 100644 index 0000000000..1e3248b4ff --- /dev/null +++ b/remote/test/puppeteer/test/src/headful.spec.ts @@ -0,0 +1,91 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {mkdtemp} from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import expect from 'expect'; +import type {PuppeteerLaunchOptions} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; + +import {getTestState, isHeadless, launch} from './mocha-utils.js'; + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); + +(!isHeadless ? describe : describe.skip)('headful tests', function () { + /* These tests fire up an actual browser so let's + * allow a higher timeout + */ + this.timeout(20_000); + + let headfulOptions: PuppeteerLaunchOptions | undefined; + let headlessOptions: PuppeteerLaunchOptions & {headless: boolean}; + + const browsers: Array<() => Promise<void>> = []; + + beforeEach(async () => { + const {defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + headfulOptions = Object.assign({}, defaultBrowserOptions, { + headless: false, + }); + headlessOptions = Object.assign({}, defaultBrowserOptions, { + headless: true, + }); + }); + + async function launchBrowser(options: any) { + const {browser, close} = await launch(options, {createContext: false}); + browsers.push(close); + return browser; + } + + afterEach(async () => { + await Promise.all( + browsers.map((close, index) => { + delete browsers[index]; + return close(); + }) + ); + }); + + describe('HEADFUL', function () { + it('headless should be able to read cookies written by headful', async () => { + /* Needs investigation into why but this fails consistently on Windows CI. */ + const {server} = await getTestState({skipLaunch: true}); + + const userDataDir = await mkdtemp(TMP_FOLDER); + // Write a cookie in headful chrome + const headfulBrowser = await launchBrowser( + Object.assign({userDataDir}, headfulOptions) + ); + const headfulPage = await headfulBrowser.newPage(); + await headfulPage.goto(server.EMPTY_PAGE); + await headfulPage.evaluate(() => { + return (document.cookie = + 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); + }); + await headfulBrowser.close(); + // Read the cookie from headless chrome + const headlessBrowser = await launchBrowser( + Object.assign({userDataDir}, headlessOptions) + ); + const headlessPage = await headlessBrowser.newPage(); + await headlessPage.goto(server.EMPTY_PAGE); + const cookie = await headlessPage.evaluate(() => { + return document.cookie; + }); + await headlessBrowser.close(); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + expect(cookie).toBe('foo=true'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/idle_override.spec.ts b/remote/test/puppeteer/test/src/idle_override.spec.ts new file mode 100644 index 0000000000..cbcfd34640 --- /dev/null +++ b/remote/test/puppeteer/test/src/idle_override.spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2020 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Emulate idle state', () => { + setupTestBrowserHooks(); + + async function getIdleState(page: Page) { + using stateElement = (await page.$('#state')) as ElementHandle<HTMLElement>; + return await page.evaluate(element => { + return element.innerText; + }, stateElement); + } + + async function verifyState(page: Page, expectedState: string) { + const actualState = await getIdleState(page); + expect(actualState).toEqual(expectedState); + } + + it('changing idle state emulation causes change of the IdleDetector state', async () => { + const {page, server, context} = await getTestState(); + await context.overridePermissions(server.PREFIX + '/idle-detector.html', [ + 'idle-detection', + ]); + + await page.goto(server.PREFIX + '/idle-detector.html'); + + // Store initial state, as soon as it is not guaranteed to be `active, unlocked`. + const initialState = await getIdleState(page); + + // Emulate Idle states and verify IdleDetector updates state accordingly. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState(page, 'Idle state: idle, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: false, + }); + await verifyState(page, 'Idle state: active, locked.'); + + await page.emulateIdleState({ + isUserActive: true, + isScreenUnlocked: true, + }); + await verifyState(page, 'Idle state: active, unlocked.'); + + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: true, + }); + await verifyState(page, 'Idle state: idle, unlocked.'); + + // Remove Idle emulation and verify IdleDetector is in initial state. + await page.emulateIdleState(); + await verifyState(page, initialState); + + // Emulate idle state again after removing emulation. + await page.emulateIdleState({ + isUserActive: false, + isScreenUnlocked: false, + }); + await verifyState(page, 'Idle state: idle, locked.'); + + // Remove emulation second time. + await page.emulateIdleState(); + await verifyState(page, initialState); + }); +}); diff --git a/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts new file mode 100644 index 0000000000..8fb557cb88 --- /dev/null +++ b/remote/test/puppeteer/test/src/ignorehttpserrors.spec.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {TLSSocket} from 'tls'; + +import expect from 'expect'; +import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; + +import {launch} from './mocha-utils.js'; + +describe('ignoreHTTPSErrors', function () { + /* Note that this test creates its own browser rather than use + * the one provided by the test set-up as we need one + * with ignoreHTTPSErrors set to true + */ + let state: Awaited<ReturnType<typeof launch>>; + + before(async () => { + state = await launch( + {ignoreHTTPSErrors: true}, + { + after: 'all', + } + ); + }); + + after(async () => { + await state.close(); + }); + + beforeEach(async () => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context.close(); + }); + + describe('Response.securityDetails', function () { + it('should work', async () => { + const {httpsServer, page} = state; + + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE), + ]); + const securityDetails = response!.securityDetails()!; + expect(securityDetails.issuer()).toBe('puppeteer-tests'); + const protocol = (serverRequest.socket as TLSSocket) + .getProtocol()! + .replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + expect(securityDetails.subjectName()).toBe('puppeteer-tests'); + expect(securityDetails.validFrom()).toBe(1589357069); + expect(securityDetails.validTo()).toBe(1904717069); + expect(securityDetails.subjectAlternativeNames()).toEqual([ + 'www.puppeteer-tests.test', + 'www.puppeteer-tests-1.test', + ]); + }); + it('should be |null| for non-secure requests', async () => { + const {server, page} = state; + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.securityDetails()).toBe(null); + }); + it('Network redirects should report SecurityDetails', async () => { + const {httpsServer, page} = state; + + httpsServer.setRedirect('/plzredirect', '/empty.html'); + const responses: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + const [serverRequest] = await Promise.all([ + httpsServer.waitForRequest('/plzredirect'), + page.goto(httpsServer.PREFIX + '/plzredirect'), + ]); + expect(responses).toHaveLength(2); + expect(responses[0]!.status()).toBe(302); + const securityDetails = responses[0]!.securityDetails()!; + const protocol = (serverRequest.socket as TLSSocket) + .getProtocol()! + .replace('v', ' '); + expect(securityDetails.protocol()).toBe(protocol); + }); + }); + + it('should work', async () => { + const {httpsServer, page} = state; + + let error!: Error; + const response = await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + expect(response.ok()).toBe(true); + }); + it('should work with request interception', async () => { + const {httpsServer, page} = state; + + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const response = (await page.goto(httpsServer.EMPTY_PAGE))!; + expect(response.status()).toBe(200); + }); + it('should work with mixed content', async () => { + const {server, httpsServer, page} = state; + + httpsServer.setRoute('/mixedcontent.html', (_req, res) => { + res.end(`<iframe src=${server.EMPTY_PAGE}></iframe>`); + }); + await page.goto(httpsServer.PREFIX + '/mixedcontent.html', { + waitUntil: 'load', + }); + expect(page.frames()).toHaveLength(2); + // Make sure blocked iframe has functional execution context + // @see https://github.com/puppeteer/puppeteer/issues/2709 + expect(await page.frames()[0]!.evaluate('1 + 2')).toBe(3); + expect(await page.frames()[1]!.evaluate('2 + 3')).toBe(5); + }); +}); diff --git a/remote/test/puppeteer/test/src/injected.spec.ts b/remote/test/puppeteer/test/src/injected.spec.ts new file mode 100644 index 0000000000..5f3696d3f6 --- /dev/null +++ b/remote/test/puppeteer/test/src/injected.spec.ts @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2022 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {LazyArg} from 'puppeteer-core/internal/common/LazyArg.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('PuppeteerUtil tests', function () { + setupTestBrowserHooks(); + + it('should work', async () => { + const {page} = await getTestState(); + + const world = page.mainFrame().isolatedRealm(); + const value = await world.evaluate( + PuppeteerUtil => { + return typeof PuppeteerUtil === 'object'; + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }) + ); + expect(value).toBeTruthy(); + }); + + describe('createFunction tests', function () { + it('should work', async () => { + const {page} = await getTestState(); + + const world = page.mainFrame().isolatedRealm(); + const value = await world.evaluate( + ({createFunction}, fnString) => { + return createFunction(fnString)(4); + }, + LazyArg.create(context => { + return context.puppeteerUtil; + }), + (() => { + return 4; + }).toString() + ); + expect(value).toBe(4); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/input.spec.ts b/remote/test/puppeteer/test/src/input.spec.ts new file mode 100644 index 0000000000..7e4cae6709 --- /dev/null +++ b/remote/test/puppeteer/test/src/input.spec.ts @@ -0,0 +1,394 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import path from 'path'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +const FILE_TO_UPLOAD = path.join(__dirname, '/../assets/file-to-upload.txt'); + +describe('input tests', function () { + setupTestBrowserHooks(); + + describe('input', function () { + it('should upload the file', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/fileupload.html'); + const filePath = path.relative(process.cwd(), FILE_TO_UPLOAD); + using input = (await page.$('input'))!; + await page.evaluate((e: HTMLElement) => { + (globalThis as any)._inputEvents = []; + e.addEventListener('change', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + e.addEventListener('input', ev => { + return (globalThis as any)._inputEvents.push(ev.type); + }); + }, input); + await input.uploadFile(filePath); + expect( + await page.evaluate((e: HTMLInputElement) => { + return e.files![0]!.name; + }, input) + ).toBe('file-to-upload.txt'); + expect( + await page.evaluate((e: HTMLInputElement) => { + return e.files![0]!.type; + }, input) + ).toBe('text/plain'); + expect( + await page.evaluate(() => { + return (globalThis as any)._inputEvents; + }) + ).toEqual(['input', 'change']); + expect( + await page.evaluate((e: HTMLInputElement) => { + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onload = fulfill); + }); + reader.readAsText(e.files![0]!); + return promise.then(() => { + return reader.result; + }); + }, input) + ).toBe('contents of the file'); + }); + }); + + describe('Page.waitForFileChooser', function () { + it('should work when file input is attached to DOM', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser).toBeTruthy(); + }); + it('should work when file input is not attached to DOM', async () => { + const {page} = await getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.evaluate(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should respect timeout', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout when there is no custom timeout', async () => { + const {page} = await getTestState(); + + page.setDefaultTimeout(1); + let error!: Error; + await page.waitForFileChooser().catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should prioritize exact timeout over default timeout', async () => { + const {page} = await getTestState(); + + page.setDefaultTimeout(0); + let error!: Error; + await page.waitForFileChooser({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with no timeout', async () => { + const {page} = await getTestState(); + + const [chooser] = await Promise.all([ + page.waitForFileChooser({timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + const el = document.createElement('input'); + el.type = 'file'; + el.click(); + }, 50); + }), + ]); + expect(chooser).toBeTruthy(); + }); + it('should return the same file chooser when there are many watchdogs simultaneously', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser1, fileChooser2] = await Promise.all([ + page.waitForFileChooser(), + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + expect(fileChooser1 === fileChooser2).toBe(true); + }); + }); + + describe('FileChooser.accept', function () { + it('should accept single file', async () => { + const {page} = await getTestState(); + + await page.setContent( + `<input type=file oninput='javascript:console.timeStamp()'>` + ); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + await Promise.all([ + chooser.accept([FILE_TO_UPLOAD]), + waitEvent(page, 'metrics'), + ]); + expect( + await page.$eval('input', input => { + return (input as HTMLInputElement).files!.length; + }) + ).toBe(1); + expect( + await page.$eval('input', input => { + return (input as HTMLInputElement).files![0]!.name; + }) + ).toBe('file-to-upload.txt'); + }); + it('should be able to read selected file', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + void page.waitForFileChooser().then(chooser => { + return chooser.accept([FILE_TO_UPLOAD]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onload = fulfill); + }); + reader.readAsText(pick.files![0]!); + return await promise.then(() => { + return reader.result; + }); + }) + ).toBe('contents of the file'); + }); + it('should be able to reset selected files with empty file list', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + void page.waitForFileChooser().then(chooser => { + return chooser.accept([FILE_TO_UPLOAD]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + return pick.files!.length; + }) + ).toBe(1); + void page.waitForFileChooser().then(chooser => { + return chooser.accept([]); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + return pick.files!.length; + }) + ).toBe(0); + }); + it('should not accept multiple files for single-file input', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error!: Error; + await chooser + .accept([ + path.relative( + process.cwd(), + __dirname + '/../assets/file-to-upload.txt' + ), + path.relative(process.cwd(), __dirname + '/../assets/pptr.png'), + ]) + .catch(error_ => { + return (error = error_); + }); + expect(error).not.toBe(null); + }); + it('should succeed even for non-existent files', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + let error!: Error; + await chooser.accept(['file-does-not-exist.txt']).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should error on read of non-existent files', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + void page.waitForFileChooser().then(chooser => { + return chooser.accept(['file-does-not-exist.txt']); + }); + expect( + await page.$eval('input', async picker => { + const pick = picker as HTMLInputElement; + pick.click(); + await new Promise(x => { + return (pick.oninput = x); + }); + const reader = new FileReader(); + const promise = new Promise(fulfill => { + return (reader.onerror = fulfill); + }); + reader.readAsText(pick.files![0]!); + return await promise.then(() => { + return false; + }); + }) + ).toBeFalsy(); + }); + it('should fail when accepting file chooser twice', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + await fileChooser.accept([]); + let error!: Error; + await fileChooser.accept([]).catch(error_ => { + return (error = error_); + }); + expect(error.message).toBe( + 'Cannot accept FileChooser which is already handled!' + ); + }); + }); + + describe('FileChooser.cancel', function () { + it('should cancel dialog', async () => { + const {page} = await getTestState(); + + // Consider file chooser canceled if we can summon another one. + // There's no reliable way in WebPlatform to see that FileChooser was + // canceled. + await page.setContent(`<input type=file>`); + const [fileChooser1] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + await fileChooser1.cancel(); + // If this resolves, than we successfully canceled file chooser. + await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLInputElement).click(); + }), + ]); + }); + it('should fail when canceling file chooser twice', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [fileChooser] = await Promise.all([ + page.waitForFileChooser(), + page.$eval('input', input => { + return (input as HTMLElement).click(); + }), + ]); + await fileChooser.cancel(); + let error!: Error; + + try { + await fileChooser.cancel(); + } catch (error_) { + error = error_ as Error; + } + + expect(error.message).toBe( + 'Cannot cancel FileChooser which is already handled!' + ); + }); + }); + + describe('FileChooser.isMultiple', () => { + it('should work for single file pick', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(false); + }); + it('should work for "multiple"', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input multiple type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + it('should work for "webkitdirectory"', async () => { + const {page} = await getTestState(); + + await page.setContent(`<input multiple webkitdirectory type=file>`); + const [chooser] = await Promise.all([ + page.waitForFileChooser(), + page.click('input'), + ]); + expect(chooser.isMultiple()).toBe(true); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/jshandle.spec.ts b/remote/test/puppeteer/test/src/jshandle.spec.ts new file mode 100644 index 0000000000..28097811e4 --- /dev/null +++ b/remote/test/puppeteer/test/src/jshandle.spec.ts @@ -0,0 +1,373 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {JSHandle} from 'puppeteer-core/internal/api/JSHandle.js'; +import { + asyncDisposeSymbol, + disposeSymbol, +} from 'puppeteer-core/internal/util/disposable.js'; +import sinon from 'sinon'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('JSHandle', function () { + setupTestBrowserHooks(); + + describe('Page.evaluateHandle', function () { + it('should work', async () => { + const {page} = await getTestState(); + + using windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle).toBeTruthy(); + }); + it('should return the RemoteObject', async () => { + const {page} = await getTestState(); + + using windowHandle = await page.evaluateHandle(() => { + return window; + }); + expect(windowHandle.remoteObject()).toBeTruthy(); + }); + it('should accept object handle as an argument', async () => { + const {page} = await getTestState(); + + using navigatorHandle = await page.evaluateHandle(() => { + return navigator; + }); + const text = await page.evaluate(e => { + return e.userAgent; + }, navigatorHandle); + expect(text).toContain('Mozilla'); + }); + it('should accept object handle to primitive types', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return 5; + }); + const isFive = await page.evaluate(e => { + return Object.is(e, 5); + }, aHandle); + expect(isFive).toBeTruthy(); + }); + it('should warn about recursive objects', async () => { + const {page} = await getTestState(); + + const test: {obj?: unknown} = {}; + test.obj = test; + let error!: Error; + await page + .evaluateHandle(opts => { + return opts; + }, test) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Recursive objects are not allowed.'); + }); + it('should accept object handle to unserializable value', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return Infinity; + }); + expect( + await page.evaluate(e => { + return Object.is(e, Infinity); + }, aHandle) + ).toBe(true); + }); + it('should use the same JS wrappers', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + (globalThis as any).FOO = 123; + return window; + }); + expect( + await page.evaluate(e => { + return (e as any).FOO; + }, aHandle) + ).toBe(123); + }); + }); + + describe('JSHandle.getProperty', function () { + it('should work', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return { + one: 1, + two: 2, + three: 3, + }; + }); + using twoHandle = await aHandle.getProperty('two'); + expect(await twoHandle.jsonValue()).toEqual(2); + }); + }); + + describe('JSHandle.jsonValue', function () { + it('should work', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return {foo: 'bar'}; + }); + const json = await aHandle.jsonValue(); + expect(json).toEqual({foo: 'bar'}); + }); + + it('works with jsonValues that are not objects', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return ['a', 'b']; + }); + const json = await aHandle.jsonValue(); + expect(json).toEqual(['a', 'b']); + }); + + it('works with jsonValues that are primitives', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return 'foo'; + }); + expect(await aHandle.jsonValue()).toEqual('foo'); + + using bHandle = await page.evaluateHandle(() => { + return undefined; + }); + expect(await bHandle.jsonValue()).toEqual(undefined); + }); + + it('should work with dates', async () => { + const {page} = await getTestState(); + + using dateHandle = await page.evaluateHandle(() => { + return new Date('2017-09-26T00:00:00.000Z'); + }); + const date = await dateHandle.jsonValue(); + expect(date).toBeInstanceOf(Date); + expect(date.toISOString()).toEqual('2017-09-26T00:00:00.000Z'); + }); + it('should not throw for circular objects', async () => { + const {page} = await getTestState(); + + using handle = await page.evaluateHandle(() => { + const t: {t?: unknown; g: number} = {g: 1}; + t.t = t; + return t; + }); + await handle.jsonValue(); + }); + }); + + describe('JSHandle.getProperties', function () { + it('should work', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return { + foo: 'bar', + }; + }); + const properties = await aHandle.getProperties(); + using foo = properties.get('foo')!; + expect(foo).toBeTruthy(); + expect(await foo.jsonValue()).toBe('bar'); + }); + it('should return even non-own properties', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + class A { + a: string; + constructor() { + this.a = '1'; + } + } + class B extends A { + b: string; + constructor() { + super(); + this.b = '2'; + } + } + return new B(); + }); + const properties = await aHandle.getProperties(); + expect(await properties.get('a')!.jsonValue()).toBe('1'); + expect(await properties.get('b')!.jsonValue()).toBe('2'); + }); + }); + + describe('JSHandle.asElement', function () { + it('should work', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return document.body; + }); + using element = aHandle.asElement(); + expect(element).toBeTruthy(); + }); + it('should return null for non-elements', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return 2; + }); + using element = aHandle.asElement(); + expect(element).toBeFalsy(); + }); + it('should return ElementHandle for TextNodes', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>ee!</div>'); + using aHandle = await page.evaluateHandle(() => { + return document.querySelector('div')!.firstChild; + }); + using element = aHandle.asElement(); + expect(element).toBeTruthy(); + expect( + await page.evaluate(e => { + return e?.nodeType === Node.TEXT_NODE; + }, element) + ); + }); + }); + + describe('JSHandle.toString', function () { + it('should work for primitives', async () => { + const {page} = await getTestState(); + + using numberHandle = await page.evaluateHandle(() => { + return 2; + }); + expect(numberHandle.toString()).toBe('JSHandle:2'); + using stringHandle = await page.evaluateHandle(() => { + return 'a'; + }); + expect(stringHandle.toString()).toBe('JSHandle:a'); + }); + it('should work for complicated objects', async () => { + const {page} = await getTestState(); + + using aHandle = await page.evaluateHandle(() => { + return window; + }); + expect(aHandle.toString()).atLeastOneToContain([ + 'JSHandle@object', + 'JSHandle@window', + ]); + }); + it('should work with different subtypes', async () => { + const {page} = await getTestState(); + + expect((await page.evaluateHandle('(function(){})')).toString()).toBe( + 'JSHandle@function' + ); + expect((await page.evaluateHandle('12')).toString()).toBe('JSHandle:12'); + expect((await page.evaluateHandle('true')).toString()).toBe( + 'JSHandle:true' + ); + expect((await page.evaluateHandle('undefined')).toString()).toBe( + 'JSHandle:undefined' + ); + expect((await page.evaluateHandle('"foo"')).toString()).toBe( + 'JSHandle:foo' + ); + expect((await page.evaluateHandle('Symbol()')).toString()).toBe( + 'JSHandle@symbol' + ); + expect((await page.evaluateHandle('new Map()')).toString()).toBe( + 'JSHandle@map' + ); + expect((await page.evaluateHandle('new Set()')).toString()).toBe( + 'JSHandle@set' + ); + expect((await page.evaluateHandle('[]')).toString()).toBe( + 'JSHandle@array' + ); + expect((await page.evaluateHandle('null')).toString()).toBe( + 'JSHandle:null' + ); + expect((await page.evaluateHandle('/foo/')).toString()).toBe( + 'JSHandle@regexp' + ); + expect((await page.evaluateHandle('document.body')).toString()).toBe( + 'JSHandle@node' + ); + expect((await page.evaluateHandle('new Date()')).toString()).toBe( + 'JSHandle@date' + ); + expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe( + 'JSHandle@weakmap' + ); + expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe( + 'JSHandle@weakset' + ); + expect((await page.evaluateHandle('new Error()')).toString()).toBe( + 'JSHandle@error' + ); + expect((await page.evaluateHandle('new Int32Array()')).toString()).toBe( + 'JSHandle@typedarray' + ); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe( + 'JSHandle@proxy' + ); + }); + }); + + describe('JSHandle[Symbol.dispose]', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('new Set()'); + const spy = sinon.spy(handle, disposeSymbol); + { + using _ = handle; + } + expect(handle).toBeInstanceOf(JSHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeTruthy(); + }); + }); + + describe('JSHandle[Symbol.asyncDispose]', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('new Set()'); + const spy = sinon.spy(handle, asyncDisposeSymbol); + { + await using _ = handle; + } + expect(handle).toBeInstanceOf(JSHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeTruthy(); + }); + }); + + describe('JSHandle.move', () => { + it('should work', async () => { + const {page} = await getTestState(); + using handle = await page.evaluateHandle('new Set()'); + const spy = sinon.spy(handle, disposeSymbol); + { + using _ = handle; + handle.move(); + } + expect(handle).toBeInstanceOf(JSHandle); + expect(spy.calledOnce).toBeTruthy(); + expect(handle.disposed).toBeFalsy(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/keyboard.spec.ts b/remote/test/puppeteer/test/src/keyboard.spec.ts new file mode 100644 index 0000000000..9157465242 --- /dev/null +++ b/remote/test/puppeteer/test/src/keyboard.spec.ts @@ -0,0 +1,550 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import os from 'os'; + +import expect from 'expect'; +import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame} from './utils.js'; + +describe('Keyboard', function () { + setupTestBrowserHooks(); + + it('should type into a textarea', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + const textarea = document.createElement('textarea'); + document.body.appendChild(textarea); + textarea.focus(); + }); + const text = 'Hello world. I am the text that was typed!'; + await page.keyboard.type(text); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe(text); + }); + it('should move with the arrow keys', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'Hello World!'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('Hello World!'); + for (const _ of 'World!') { + await page.keyboard.press('ArrowLeft'); + } + await page.keyboard.type('inserted '); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('Hello inserted World!'); + await page.keyboard.down('Shift'); + for (const _ of 'inserted ') { + await page.keyboard.press('ArrowLeft'); + } + await page.keyboard.up('Shift'); + await page.keyboard.press('Backspace'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('Hello World!'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/1313 + it('should trigger commands of keyboard shortcuts', async () => { + const {page, server} = await getTestState(); + const cmdKey = os.platform() === 'darwin' ? 'Meta' : 'Control'; + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', 'hello'); + + await page.keyboard.down(cmdKey); + await page.keyboard.press('a', {commands: ['SelectAll']}); + await page.keyboard.up(cmdKey); + + await page.keyboard.down(cmdKey); + await page.keyboard.down('c', {commands: ['Copy']}); + await page.keyboard.up('c'); + await page.keyboard.up(cmdKey); + + await page.keyboard.down(cmdKey); + await page.keyboard.press('v', {commands: ['Paste']}); + await page.keyboard.press('v', {commands: ['Paste']}); + await page.keyboard.up(cmdKey); + + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('hellohello'); + }); + it('should send a character with ElementHandle.press', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + using textarea = (await page.$('textarea'))!; + await textarea.press('a'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('a'); + + await page.evaluate(() => { + return window.addEventListener( + 'keydown', + e => { + return e.preventDefault(); + }, + true + ); + }); + + await textarea.press('b'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('a'); + }); + it('ElementHandle.press should not support |text| option', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + using textarea = (await page.$('textarea'))!; + await textarea.press('a', {text: 'ё'}); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')!.value; + }) + ).toBe('a'); + }); + it('should send a character with sendCharacter', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + + await page.evaluate(() => { + (globalThis as any).inputCount = 0; + (globalThis as any).keyDownCount = 0; + window.addEventListener( + 'input', + () => { + (globalThis as any).inputCount += 1; + }, + true + ); + window.addEventListener( + 'keydown', + () => { + (globalThis as any).keyDownCount += 1; + }, + true + ); + }); + + await page.keyboard.sendCharacter('嗨'); + expect( + await page.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; + }) + ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0}); + + await page.keyboard.sendCharacter('a'); + expect( + await page.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; + }) + ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0}); + }); + it('should send a character with sendCharacter in iframe', async () => { + this.timeout(2000); + + const {page} = await getTestState(); + + await page.setContent(` + <iframe srcdoc="<iframe name='test' srcdoc='<textarea></textarea>'></iframe>"</iframe> + `); + const frame = await page.waitForFrame(frame => { + return frame.name() === 'test'; + }); + await frame.focus('textarea'); + + await frame.evaluate(() => { + (globalThis as any).inputCount = 0; + (globalThis as any).keyDownCount = 0; + window.addEventListener( + 'input', + () => { + (globalThis as any).inputCount += 1; + }, + true + ); + window.addEventListener( + 'keydown', + () => { + (globalThis as any).keyDownCount += 1; + }, + true + ); + }); + + await page.keyboard.sendCharacter('嗨'); + expect( + await frame.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; + }) + ).toMatchObject({value: '嗨', inputs: 1, keyDowns: 0}); + + await page.keyboard.sendCharacter('a'); + expect( + await frame.$eval('textarea', textarea => { + return { + value: textarea.value, + inputs: (globalThis as any).inputCount, + keyDowns: (globalThis as any).keyDownCount, + }; + }) + ).toMatchObject({value: '嗨a', inputs: 2, keyDowns: 0}); + }); + it('should report shiftKey', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + const codeForKey = new Set<KeyInput>(['Shift', 'Alt', 'Control']); + for (const modifierKey of codeForKey) { + await keyboard.down(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe(`Keydown: ${modifierKey} ${modifierKey}Left [${modifierKey}]`); + await keyboard.down('!'); + if (modifierKey === 'Shift') { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + `Keydown: ! Digit1 [${modifierKey}]\n` + `input: ! insertText false` + ); + } else { + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe(`Keydown: ! Digit1 [${modifierKey}]`); + } + + await keyboard.up('!'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe(`Keyup: ! Digit1 [${modifierKey}]`); + await keyboard.up(modifierKey); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe(`Keyup: ${modifierKey} ${modifierKey}Left []`); + } + }); + it('should report multiple modifiers', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Control'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: Control ControlLeft [Control]'); + await keyboard.down('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: Alt AltLeft [Alt Control]'); + await keyboard.down(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keydown: ; Semicolon [Alt Control]'); + await keyboard.up(';'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: ; Semicolon [Alt Control]'); + await keyboard.up('Control'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: Control ControlLeft [Alt]'); + await keyboard.up('Alt'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe('Keyup: Alt AltLeft []'); + }); + it('should send proper codes while typing', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + await page.keyboard.type('!'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + [ + 'Keydown: ! Digit1 []', + 'input: ! insertText false', + 'Keyup: ! Digit1 []', + ].join('\n') + ); + await page.keyboard.type('^'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + [ + 'Keydown: ^ Digit6 []', + 'input: ^ insertText false', + 'Keyup: ^ Digit6 []', + ].join('\n') + ); + }); + it('should send proper codes while typing with shift', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/keyboard.html'); + const keyboard = page.keyboard; + await keyboard.down('Shift'); + await page.keyboard.type('~'); + expect( + await page.evaluate(() => { + return (globalThis as any).getResult(); + }) + ).toBe( + [ + 'Keydown: Shift ShiftLeft [Shift]', + 'Keydown: ~ Backquote [Shift]', + 'input: ~ insertText false', + 'Keyup: ~ Backquote [Shift]', + ].join('\n') + ); + await keyboard.up('Shift'); + }); + it('should not type canceled events', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + event => { + event.stopPropagation(); + event.stopImmediatePropagation(); + if (event.key === 'l') { + event.preventDefault(); + } + if (event.key === 'o') { + event.preventDefault(); + } + }, + false + ); + }); + await page.keyboard.type('Hello World!'); + expect( + await page.evaluate(() => { + return (globalThis as any).textarea.value; + }) + ).toBe('He Wrd!'); + }); + it('should specify repeat property', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + await page.evaluate(() => { + return document.querySelector('textarea')!.addEventListener( + 'keydown', + e => { + return ((globalThis as any).lastEvent = e); + }, + true + ); + }); + await page.keyboard.down('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + await page.keyboard.press('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(true); + + await page.keyboard.down('b'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + await page.keyboard.down('b'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(true); + + await page.keyboard.up('a'); + await page.keyboard.down('a'); + expect( + await page.evaluate(() => { + return (globalThis as any).lastEvent.repeat; + }) + ).toBe(false); + }); + it('should type all kinds of characters', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.focus('textarea'); + const text = 'This text goes onto two lines.\nThis character is 嗨.'; + await page.keyboard.type(text); + expect(await page.evaluate('result')).toBe(text); + }); + it('should specify location', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.evaluate(() => { + window.addEventListener( + 'keydown', + event => { + return ((globalThis as any).keyLocation = event.location); + }, + true + ); + }); + using textarea = (await page.$('textarea'))!; + + await textarea.press('Digit5'); + expect(await page.evaluate('keyLocation')).toBe(0); + + await textarea.press('ControlLeft'); + expect(await page.evaluate('keyLocation')).toBe(1); + + await textarea.press('ControlRight'); + expect(await page.evaluate('keyLocation')).toBe(2); + + await textarea.press('NumpadSubtract'); + expect(await page.evaluate('keyLocation')).toBe(3); + }); + it('should throw on unknown keys', async () => { + const {page} = await getTestState(); + + const error = await page.keyboard + // @ts-expect-error bad input + .press('NotARealKey') + .catch(error_ => { + return error_; + }); + expect(error.message).toBe('Unknown key: "NotARealKey"'); + }); + it('should type emoji', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + await page.type('textarea', '👹 Tokyo street Japan 🇯🇵'); + expect( + await page.$eval('textarea', textarea => { + return textarea.value; + }) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should type emoji into an iframe', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame( + page, + 'emoji-test', + server.PREFIX + '/input/textarea.html' + ); + const frame = page.frames()[1]!; + using textarea = (await frame.$('textarea'))!; + await textarea.type('👹 Tokyo street Japan 🇯🇵'); + expect( + await frame.$eval('textarea', textarea => { + return textarea.value; + }) + ).toBe('👹 Tokyo street Japan 🇯🇵'); + }); + it('should press the meta key', async () => { + // This test only makes sense on macOS. + if (os.platform() !== 'darwin') { + return; + } + const {page} = await getTestState(); + + await page.evaluate(() => { + (globalThis as any).result = null; + document.addEventListener('keydown', event => { + (globalThis as any).result = [event.key, event.code, event.metaKey]; + }); + }); + await page.keyboard.press('Meta'); + // Have to do this because we lose a lot of type info when evaluating a + // string not a function. This is why functions are recommended rather than + // using strings (although we'll leave this test so we have coverage of both + // approaches.) + const [key, code, metaKey] = (await page.evaluate('result')) as [ + string, + string, + boolean, + ]; + expect(key).toBe('Meta'); + expect(code).toBe('MetaLeft'); + expect(metaKey).toBe(true); + }); +}); diff --git a/remote/test/puppeteer/test/src/launcher.spec.ts b/remote/test/puppeteer/test/src/launcher.spec.ts new file mode 100644 index 0000000000..f31b22b1e5 --- /dev/null +++ b/remote/test/puppeteer/test/src/launcher.spec.ts @@ -0,0 +1,1025 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert'; +import fs from 'fs'; +import {mkdtemp, readFile, writeFile} from 'fs/promises'; +import os from 'os'; +import path from 'path'; +import type {TLSSocket} from 'tls'; + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import sinon from 'sinon'; + +import {getTestState, isHeadless, launch} from './mocha-utils.js'; +import {dumpFrames, waitEvent} from './utils.js'; + +const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-'); +const FIREFOX_TIMEOUT = 30_000; + +describe('Launcher specs', function () { + this.timeout(FIREFOX_TIMEOUT); + + describe('Puppeteer', function () { + describe('Browser.disconnect', function () { + it('should reject navigation when browser closes', async () => { + const {browser, close, puppeteer, server} = await launch({}); + server.setRoute('/one-style.css', () => {}); + try { + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + const page = await remote.newPage(); + const navigationPromise = page + .goto(server.PREFIX + '/one-style.html', {timeout: 60000}) + .catch(error_ => { + return error_; + }); + await server.waitForRequest('/one-style.css'); + await remote.disconnect(); + const error = await navigationPromise; + expect( + [ + 'Navigating frame was detached', + 'Protocol error (Page.navigate): Target closed.', + ].includes(error.message) + ).toBeTruthy(); + } finally { + await close(); + } + }); + it('should reject waitForSelector when browser closes', async () => { + const {browser, close, server, puppeteer} = await launch({}); + server.setRoute('/empty.html', () => {}); + try { + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + const page = await remote.newPage(); + const watchdog = page + .waitForSelector('div', {timeout: 60000}) + .catch(error_ => { + return error_; + }); + await remote.disconnect(); + const error = await watchdog; + expect(error.message).toContain('Session closed.'); + } finally { + await close(); + } + }); + }); + describe('Browser.close', function () { + it('should terminate network waiters', async () => { + const {browser, close, server, puppeteer} = await launch({}); + try { + const remote = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + const newPage = await remote.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(error => { + return error; + }), + newPage.waitForResponse(server.EMPTY_PAGE).catch(error => { + return error; + }), + browser.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).atLeastOneToContain([ + 'Target closed', + 'Page closed!', + ]); + expect(message).not.toContain('Timeout'); + } + } finally { + await close(); + } + }); + }); + describe('Puppeteer.launch', function () { + it('can launch and close the browser', async () => { + const {close} = await launch({}); + await close(); + }); + it('should have default url when launching browser', async function () { + const {browser, close} = await launch({}, {createContext: false}); + try { + const pages = (await browser.pages()).map( + (page: {url: () => any}) => { + return page.url(); + } + ); + expect(pages).toEqual(['about:blank']); + } finally { + await close(); + } + }); + it('should close browser with beforeunload page', async () => { + const {browser, server, close} = await launch( + {}, + {createContext: false} + ); + try { + const page = await browser.newPage(); + + await page.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await page.click('body'); + } finally { + await close(); + } + }); + it('should reject all promises when browser is closed', async () => { + const {page, close} = await launch({}); + let error!: Error; + const neverResolves = page + .evaluate(() => { + return new Promise(() => {}); + }) + .catch(error_ => { + return (error = error_); + }); + await close(); + await neverResolves; + expect(error.message).toContain('Protocol error'); + }); + it('should reject if executable path is invalid', async () => { + let waitError!: Error; + await launch({ + executablePath: 'random-invalid-path', + }).catch(error => { + return (waitError = error); + }); + expect(waitError.message).toContain('Failed to launch'); + }); + it('userDataDir option', async () => { + const userDataDir = await mkdtemp(TMP_FOLDER); + const {context, close} = await launch({userDataDir}); + // Open a page to make sure its functional. + try { + await context.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + } finally { + await close(); + } + + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('tmp profile should be cleaned up', async () => { + const {puppeteer} = await getTestState({skipLaunch: true}); + + // Set a custom test tmp dir so that we can validate that + // the profile dir is created and then cleaned up. + const testTmpDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'puppeteer_test_chrome_profile-') + ); + const oldTmpDir = puppeteer.configuration.temporaryDirectory; + puppeteer.configuration.temporaryDirectory = testTmpDir; + + // Path should be empty before starting the browser. + expect(fs.readdirSync(testTmpDir)).toHaveLength(0); + const {context, close} = await launch({}); + try { + // One profile folder should have been created at this moment. + const profiles = fs.readdirSync(testTmpDir); + expect(profiles).toHaveLength(1); + expect(profiles[0]?.startsWith('puppeteer_dev_chrome_profile-')).toBe( + true + ); + + // Open a page to make sure its functional. + await context.newPage(); + } finally { + await close(); + } + + // Profile should be deleted after closing the browser + expect(fs.readdirSync(testTmpDir)).toHaveLength(0); + + // Restore env var + puppeteer.configuration.temporaryDirectory = oldTmpDir; + }); + it('userDataDir option restores preferences', async () => { + const userDataDir = await mkdtemp(TMP_FOLDER); + + const prefsJSPath = path.join(userDataDir, 'prefs.js'); + const prefsJSContent = 'user_pref("browser.warnOnQuit", true)'; + await writeFile(prefsJSPath, prefsJSContent); + + const {context, close} = await launch({userDataDir}); + try { + // Open a page to make sure its functional. + await context.newPage(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + + expect(await readFile(prefsJSPath, 'utf8')).toBe(prefsJSContent); + } finally { + await close(); + } + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir argument', async () => { + const {isChrome, defaultBrowserOptions: options} = await getTestState({ + skipLaunch: true, + }); + + const userDataDir = await mkdtemp(TMP_FOLDER); + if (isChrome) { + options.args = [ + ...(options.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [...(options.args || []), '-profile', userDataDir]; + } + const {close} = await launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir argument with non-existent dir', async () => { + const {isChrome, defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + + const userDataDir = await mkdtemp(TMP_FOLDER); + rmSync(userDataDir); + const options = Object.assign({}, defaultBrowserOptions); + if (isChrome) { + options.args = [ + ...(defaultBrowserOptions.args || []), + `--user-data-dir=${userDataDir}`, + ]; + } else { + options.args = [ + ...(defaultBrowserOptions.args || []), + '-profile', + userDataDir, + ]; + } + const {close} = await launch(options); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + await close(); + expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0); + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir option should restore state', async () => { + const userDataDir = await mkdtemp(TMP_FOLDER); + const {server, browser, close} = await launch({userDataDir}); + try { + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (localStorage['hey'] = 'hello'); + }); + } finally { + await close(); + } + + const {browser: browser2, close: close2} = await launch({userDataDir}); + + try { + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect( + await page2.evaluate(() => { + return localStorage['hey']; + }) + ).toBe('hello'); + } finally { + await close2(); + } + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('userDataDir option should restore cookies', async () => { + const userDataDir = await mkdtemp(TMP_FOLDER); + const {server, browser, close} = await launch({userDataDir}); + try { + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return (document.cookie = + 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT'); + }); + } finally { + await close(); + } + + const {browser: browser2, close: close2} = await launch({userDataDir}); + try { + const page2 = await browser2.newPage(); + await page2.goto(server.EMPTY_PAGE); + expect( + await page2.evaluate(() => { + return document.cookie; + }) + ).toBe('doSomethingOnlyOnce=true'); + } finally { + await close2(); + } + + // This might throw. See https://github.com/puppeteer/puppeteer/issues/2778 + try { + rmSync(userDataDir); + } catch {} + }); + it('should return the default arguments', async () => { + const {isChrome, isFirefox, puppeteer} = await getTestState({ + skipLaunch: true, + }); + + if (isChrome) { + expect(puppeteer.defaultArgs()).toContain('--no-first-run'); + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + `--user-data-dir=${path.resolve('foo')}` + ); + } else if (isFirefox) { + expect(puppeteer.defaultArgs()).toContain('--headless'); + expect(puppeteer.defaultArgs()).toContain('--no-remote'); + if (os.platform() === 'darwin') { + expect(puppeteer.defaultArgs()).toContain('--foreground'); + } else { + expect(puppeteer.defaultArgs()).not.toContain('--foreground'); + } + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '--headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + '--profile' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain('foo'); + } else { + expect(puppeteer.defaultArgs()).toContain('-headless'); + expect(puppeteer.defaultArgs({headless: false})).not.toContain( + '-headless' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + '-profile' + ); + expect(puppeteer.defaultArgs({userDataDir: 'foo'})).toContain( + path.resolve('foo') + ); + } + }); + it('should report the correct product', async () => { + const {isChrome, isFirefox, puppeteer} = await getTestState({ + skipLaunch: true, + }); + if (isChrome) { + expect(puppeteer.product).toBe('chrome'); + } else if (isFirefox) { + expect(puppeteer.product).toBe('firefox'); + } + }); + (!isHeadless ? it : it.skip)( + 'should work with no default arguments', + async () => { + const {context, close} = await launch({ + ignoreDefaultArgs: true, + }); + try { + const page = await context.newPage(); + expect(await page.evaluate('11 * 11')).toBe(121); + await page.close(); + } finally { + await close(); + } + } + ); + it('should filter out ignored default arguments in Chrome', async () => { + const {defaultBrowserOptions, puppeteer} = await getTestState({ + skipLaunch: true, + }); + // Make sure we launch with `--enable-automation` by default. + const defaultArgs = puppeteer.defaultArgs(); + const {browser, close} = await launch( + Object.assign({}, defaultBrowserOptions, { + // Ignore first and third default argument. + ignoreDefaultArgs: [defaultArgs[0]!, defaultArgs[2]], + }) + ); + try { + const spawnargs = browser.process()!.spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1); + expect(spawnargs.indexOf(defaultArgs[2]!)).toBe(-1); + } finally { + await close(); + } + }); + it('should filter out ignored default argument in Firefox', async () => { + const {defaultBrowserOptions, puppeteer} = await getTestState({ + skipLaunch: true, + }); + + const defaultArgs = puppeteer.defaultArgs(); + const {browser, close} = await launch( + Object.assign({}, defaultBrowserOptions, { + // Only the first argument is fixed, others are optional. + ignoreDefaultArgs: [defaultArgs[0]!], + }) + ); + try { + const spawnargs = browser.process()!.spawnargs; + if (!spawnargs) { + throw new Error('spawnargs not present'); + } + expect(spawnargs.indexOf(defaultArgs[0]!)).toBe(-1); + expect(spawnargs.indexOf(defaultArgs[1]!)).not.toBe(-1); + } finally { + await close(); + } + }); + it('should have default URL when launching browser', async function () { + const {browser, close} = await launch( + {}, + { + createContext: false, + } + ); + try { + const pages = (await browser.pages()).map(page => { + return page.url(); + }); + expect(pages).toEqual(['about:blank']); + } finally { + await close(); + } + }); + it('should have custom URL when launching browser', async () => { + const {server, defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + + const options = Object.assign({}, defaultBrowserOptions); + options.args = [server.EMPTY_PAGE].concat(options.args || []); + const {browser, close} = await launch(options, { + createContext: false, + }); + try { + const pages = await browser.pages(); + expect(pages).toHaveLength(1); + const page = pages[0]!; + if (page.url() !== server.EMPTY_PAGE) { + await page.waitForNavigation(); + } + expect(page.url()).toBe(server.EMPTY_PAGE); + } finally { + await close(); + } + }); + it('should pass the timeout parameter to browser.waitForTarget', async () => { + const options = { + timeout: 1, + }; + let error!: Error; + await launch(options).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with timeout = 0', async () => { + const {close} = await launch({ + timeout: 0, + }); + await close(); + }); + it('should set the default viewport', async () => { + const {context, close} = await launch({ + defaultViewport: { + width: 456, + height: 789, + }, + }); + + try { + const page = await context.newPage(); + expect(await page.evaluate('window.innerWidth')).toBe(456); + expect(await page.evaluate('window.innerHeight')).toBe(789); + } finally { + await close(); + } + }); + it('should disable the default viewport', async () => { + const {context, close} = await launch({ + defaultViewport: null, + }); + try { + const page = await context.newPage(); + expect(page.viewport()).toBe(null); + } finally { + await close(); + } + }); + it('should take fullPage screenshots when defaultViewport is null', async () => { + const {server, context, close} = await launch({ + defaultViewport: null, + }); + try { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeInstanceOf(Buffer); + } finally { + await close(); + } + }); + it('should set the debugging port', async () => { + const {browser, close} = await launch({ + defaultViewport: null, + debuggingPort: 9999, + }); + try { + const url = new URL(browser.wsEndpoint()); + expect(url.port).toBe('9999'); + } finally { + await close(); + } + }); + it('should not allow setting debuggingPort and pipe', async () => { + const options = { + defaultViewport: null, + debuggingPort: 9999, + pipe: true, + }; + let error!: Error; + await launch(options).catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('either pipe or debugging port'); + }); + (!isHeadless ? it : it.skip)( + 'should launch Chrome properly with --no-startup-window and waitForInitialPage=false', + async () => { + const {defaultBrowserOptions} = await getTestState({ + skipLaunch: true, + }); + const options = { + waitForInitialPage: false, + // This is needed to prevent Puppeteer from adding an initial blank page. + // See also https://github.com/puppeteer/puppeteer/blob/ad6b736039436fcc5c0a262e5b575aa041427be3/src/node/Launcher.ts#L200 + ignoreDefaultArgs: true, + ...defaultBrowserOptions, + args: ['--no-startup-window'], + }; + const {browser, close} = await launch(options, { + createContext: false, + }); + try { + const pages = await browser.pages(); + expect(pages).toHaveLength(0); + } finally { + await close(); + } + } + ); + }); + + describe('Puppeteer.launch', function () { + it('should be able to launch Chrome', async () => { + const {browser, close} = await launch({product: 'chrome'}); + try { + const userAgent = await browser.userAgent(); + expect(userAgent).toContain('Chrome'); + } finally { + await close(); + } + }); + + it('should be able to launch Firefox', async function () { + this.timeout(FIREFOX_TIMEOUT); + const {browser, close} = await launch({product: 'firefox'}); + try { + const userAgent = await browser.userAgent(); + expect(userAgent).toContain('Firefox'); + } finally { + await close(); + } + }); + }); + + describe('Puppeteer.connect', function () { + it('should be able to connect multiple times to the same browser', async () => { + const {puppeteer, browser, close} = await launch({}); + try { + const otherBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + const page = await otherBrowser.newPage(); + expect( + await page.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + await otherBrowser.disconnect(); + + const secondPage = await browser.newPage(); + expect( + await secondPage.evaluate(() => { + return 7 * 6; + }) + ).toBe(42); + } finally { + await close(); + } + }); + it('should be able to close remote browser', async () => { + const {puppeteer, browser, close} = await launch({}); + try { + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + await Promise.all([ + waitEvent(browser, 'disconnected'), + remoteBrowser.close(), + ]); + } finally { + await close(); + } + }); + it('should be able to connect to a browser with no page targets', async () => { + const {puppeteer, browser, close} = await launch({}); + + try { + const pages = await browser.pages(); + await Promise.all( + pages.map(page => { + return page.close(); + }) + ); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint: browser.wsEndpoint(), + protocol: browser.protocol, + }); + await Promise.all([ + waitEvent(browser, 'disconnected'), + remoteBrowser.close(), + ]); + } finally { + await close(); + } + }); + it('should support ignoreHTTPSErrors option', async () => { + const {puppeteer, httpsServer, browser, close} = await launch( + {}, + { + createContext: false, + } + ); + + try { + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + ignoreHTTPSErrors: true, + protocol: browser.protocol, + }); + const page = await remoteBrowser.newPage(); + const [serverRequest, response] = await Promise.all([ + httpsServer.waitForRequest('/empty.html'), + page.goto(httpsServer.EMPTY_PAGE), + ]); + expect(response!.ok()).toBe(true); + expect(response!.securityDetails()).toBeTruthy(); + const protocol = (serverRequest.socket as TLSSocket) + .getProtocol()! + .replace('v', ' '); + expect(response!.securityDetails()!.protocol()).toBe(protocol); + await page.close(); + await remoteBrowser.close(); + } finally { + await close(); + } + }); + + it('should support targetFilter option in puppeteer.launch', async () => { + const {browser, close} = await launch( + { + targetFilter: target => { + return target.type() !== 'page'; + }, + waitForInitialPage: false, + }, + {createContext: false} + ); + try { + const targets = browser.targets(); + expect(targets).toHaveLength(1); + expect( + targets.find(target => { + return target.type() === 'page'; + }) + ).toBeUndefined(); + } finally { + await close(); + } + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4197 + it('should support targetFilter option', async () => { + const {puppeteer, server, browser, close} = await launch( + {}, + { + createContext: false, + } + ); + try { + const browserWSEndpoint = browser.wsEndpoint(); + const page1 = await browser.newPage(); + await page1.goto(server.EMPTY_PAGE); + + const page2 = await browser.newPage(); + await page2.goto(server.EMPTY_PAGE + '?should-be-ignored'); + + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + targetFilter: target => { + return !target.url().includes('should-be-ignored'); + }, + protocol: browser.protocol, + }); + + const pages = await remoteBrowser.pages(); + + expect( + pages + .map((p: Page) => { + return p.url(); + }) + .sort() + ).toEqual(['about:blank', server.EMPTY_PAGE]); + + await page2.close(); + await page1.close(); + await remoteBrowser.disconnect(); + await browser.close(); + } finally { + await close(); + } + }); + it('should be able to reconnect to a disconnected browser', async () => { + const {puppeteer, server, browser, close} = await launch({}); + try { + const browserWSEndpoint = browser.wsEndpoint(); + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + await browser.disconnect(); + + const remoteBrowser = await puppeteer.connect({ + browserWSEndpoint, + protocol: browser.protocol, + }); + const pages = await remoteBrowser.pages(); + const restoredPage = pages.find(page => { + return page.url() === server.PREFIX + '/frames/nested-frames.html'; + })!; + expect(dumpFrames(restoredPage.mainFrame())).toEqual([ + 'http://localhost:<PORT>/frames/nested-frames.html', + ' http://localhost:<PORT>/frames/two-frames.html (2frames)', + ' http://localhost:<PORT>/frames/frame.html (uno)', + ' http://localhost:<PORT>/frames/frame.html (dos)', + ' http://localhost:<PORT>/frames/frame.html (aframe)', + ]); + expect( + await restoredPage.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + await remoteBrowser.close(); + } finally { + await close(); + } + }); + // @see https://github.com/puppeteer/puppeteer/issues/4197#issuecomment-481793410 + it('should be able to connect to the same page simultaneously', async () => { + const {puppeteer, browser: browserOne, close} = await launch({}); + + try { + const browserTwo = await puppeteer.connect({ + browserWSEndpoint: browserOne.wsEndpoint(), + protocol: browserOne.protocol, + }); + const [page1, page2] = await Promise.all([ + new Promise<Page | null>(x => { + return browserOne.once('targetcreated', target => { + x(target.page()); + }); + }), + browserTwo.newPage(), + ]); + assert(page1); + expect( + await page1.evaluate(() => { + return 7 * 8; + }) + ).toBe(56); + expect( + await page2.evaluate(() => { + return 7 * 6; + }) + ).toBe(42); + } finally { + await close(); + } + }); + it('should be able to reconnect', async () => { + const { + puppeteer, + server, + browser: browserOne, + close, + } = await launch({}); + try { + const browserWSEndpoint = browserOne.wsEndpoint(); + const pageOne = await browserOne.newPage(); + await pageOne.goto(server.EMPTY_PAGE); + await browserOne.disconnect(); + + const browserTwo = await puppeteer.connect({ + browserWSEndpoint, + protocol: browserOne.protocol, + }); + const pages = await browserTwo.pages(); + const pageTwo = pages.find(page => { + return page.url() === server.EMPTY_PAGE; + })!; + await pageTwo.reload(); + using _ = await pageTwo.waitForSelector('body', { + timeout: 10000, + }); + await browserTwo.close(); + } finally { + await close(); + } + }); + }); + describe('Puppeteer.executablePath', function () { + it('should work', async () => { + const {puppeteer} = await getTestState({ + skipLaunch: true, + }); + + const executablePath = puppeteer.executablePath(); + expect(fs.existsSync(executablePath)).toBe(true); + expect(fs.realpathSync(executablePath)).toBe(executablePath); + }); + it('returns executablePath for channel', async () => { + const {puppeteer} = await getTestState({ + skipLaunch: true, + }); + + const executablePath = puppeteer.executablePath('chrome'); + expect(executablePath).toBeTruthy(); + }); + describe('when executable path is configured', () => { + const sandbox = sinon.createSandbox(); + + beforeEach(async () => { + const {puppeteer} = await getTestState({ + skipLaunch: true, + }); + sandbox + .stub(puppeteer.configuration, 'executablePath') + .value('SOME_CUSTOM_EXECUTABLE'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('its value is used', async () => { + const {puppeteer} = await getTestState({ + skipLaunch: true, + }); + try { + puppeteer.executablePath(); + } catch (error) { + expect((error as Error).message).toContain( + 'SOME_CUSTOM_EXECUTABLE' + ); + } + }); + }); + }); + }); + + describe('Browser target events', function () { + it('should work', async () => { + const {browser, server, close} = await launch({}); + + try { + const events: string[] = []; + browser.on('targetcreated', () => { + events.push('CREATED'); + }); + browser.on('targetchanged', () => { + events.push('CHANGED'); + }); + browser.on('targetdestroyed', () => { + events.push('DESTROYED'); + }); + const page = await browser.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + expect(events).toEqual(['CREATED', 'CHANGED', 'DESTROYED']); + } finally { + await close(); + } + }); + }); + + describe('Browser.Events.disconnected', function () { + it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async () => { + const {puppeteer, browser, close} = await launch({}); + try { + const browserWSEndpoint = browser.wsEndpoint(); + const remoteBrowser1 = await puppeteer.connect({ + browserWSEndpoint, + protocol: browser.protocol, + }); + const remoteBrowser2 = await puppeteer.connect({ + browserWSEndpoint, + protocol: browser.protocol, + }); + + let disconnectedOriginal = 0; + let disconnectedRemote1 = 0; + let disconnectedRemote2 = 0; + browser.on('disconnected', () => { + ++disconnectedOriginal; + }); + remoteBrowser1.on('disconnected', () => { + ++disconnectedRemote1; + }); + remoteBrowser2.on('disconnected', () => { + ++disconnectedRemote2; + }); + + await Promise.all([ + waitEvent(remoteBrowser2, 'disconnected'), + remoteBrowser2.disconnect(), + ]); + + expect(disconnectedOriginal).toBe(0); + expect(disconnectedRemote1).toBe(0); + expect(disconnectedRemote2).toBe(1); + + await Promise.all([ + waitEvent(remoteBrowser1, 'disconnected'), + waitEvent(browser, 'disconnected'), + browser.close(), + ]); + + expect(disconnectedOriginal).toBe(1); + expect(disconnectedRemote1).toBe(1); + expect(disconnectedRemote2).toBe(1); + } finally { + await close(); + } + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/locator.spec.ts b/remote/test/puppeteer/test/src/locator.spec.ts new file mode 100644 index 0000000000..9b00cc2d7c --- /dev/null +++ b/remote/test/puppeteer/test/src/locator.spec.ts @@ -0,0 +1,763 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import {TimeoutError} from 'puppeteer-core'; +import { + Locator, + LocatorEvent, +} from 'puppeteer-core/internal/api/locators/locators.js'; +import sinon from 'sinon'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Locator', function () { + setupTestBrowserHooks(); + + it('should work with a frame', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onclick="this.innerText = 'clicked';">test</button> + `); + let willClick = false; + await page + .mainFrame() + .locator('button') + .on(LocatorEvent.Action, () => { + willClick = true; + }) + .click(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + expect(willClick).toBe(true); + }); + + it('should work without preconditions', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onclick="this.innerText = 'clicked';">test</button> + `); + let willClick = false; + await page + .locator('button') + .setEnsureElementIsInTheViewport(false) + .setTimeout(0) + .setVisibility(null) + .setWaitForEnabled(false) + .setWaitForStableBoundingBox(false) + .on(LocatorEvent.Action, () => { + willClick = true; + }) + .click(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + expect(willClick).toBe(true); + }); + + describe('Locator.click', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onclick="this.innerText = 'clicked';">test</button> + `); + let willClick = false; + await page + .locator('button') + .on(LocatorEvent.Action, () => { + willClick = true; + }) + .click(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + expect(willClick).toBe(true); + }); + + it('should work for multiple selectors', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onclick="this.innerText = 'clicked';">test</button> + `); + let clicked = false; + await page + .locator('::-p-text(test), ::-p-xpath(/button)') + .on(LocatorEvent.Action, () => { + clicked = true; + }) + .click(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + expect(clicked).toBe(true); + }); + + it('should work if the element is out of viewport', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="margin-top: 600px;" onclick="this.innerText = 'clicked';">test</button> + `); + await page.locator('button').click(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + }); + + it('should work if the element becomes visible later', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="display: none;" onclick="this.innerText = 'clicked';">test</button> + `); + using button = await page.$('button'); + const result = page + .locator('button') + .click() + .catch(err => { + return err; + }); + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('test'); + await button?.evaluate(el => { + el.style.display = 'block'; + }); + const maybeError = await result; + if (maybeError instanceof Error) { + throw maybeError; + } + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('clicked'); + }); + + it('should work if the element becomes enabled later', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button disabled onclick="this.innerText = 'clicked';">test</button> + `); + using button = await page.$('button'); + const result = page.locator('button').click(); + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('test'); + await button?.evaluate(el => { + el.disabled = false; + }); + await result; + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('clicked'); + }); + + it('should work if multiple conditions are satisfied later', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="margin-top: 600px;" style="display: none;" disabled onclick="this.innerText = 'clicked';">test</button> + `); + using button = await page.$('button'); + const result = page.locator('button').click(); + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('test'); + await button?.evaluate(el => { + el.disabled = false; + el.style.display = 'block'; + }); + await result; + expect( + await button?.evaluate(el => { + return el.innerText; + }) + ).toBe('clicked'); + }); + + it('should time out', async () => { + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + const {page} = await getTestState(); + + page.setDefaultTimeout(5000); + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="display: none;" onclick="this.innerText = 'clicked';">test</button> + `); + const result = page.locator('button').click(); + clock.tick(5100); + await expect(result).rejects.toEqual( + new TimeoutError('Timed out after waiting 5000ms') + ); + } finally { + clock.restore(); + } + }); + + it('should retry clicks on errors', async () => { + const {page} = await getTestState(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + page.setDefaultTimeout(5000); + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="display: none;" onclick="this.innerText = 'clicked';">test</button> + `); + const result = page.locator('button').click(); + clock.tick(5100); + await expect(result).rejects.toEqual( + new TimeoutError('Timed out after waiting 5000ms') + ); + } finally { + clock.restore(); + } + }); + + it('can be aborted', async () => { + const {page} = await getTestState(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + page.setDefaultTimeout(5000); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="display: none;" onclick="this.innerText = 'clicked';">test</button> + `); + const abortController = new AbortController(); + const result = page.locator('button').click({ + signal: abortController.signal, + }); + clock.tick(2000); + abortController.abort(); + await expect(result).rejects.toThrow(/aborted/); + } finally { + clock.restore(); + } + }); + + it('should work with a OOPIF', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <iframe src="data:text/html,<button onclick="this.innerText = 'clicked';">test</button>"></iframe> + `); + const frame = await page.waitForFrame(frame => { + return frame.url().startsWith('data'); + }); + let willClick = false; + await frame + .locator('button') + .on(LocatorEvent.Action, () => { + willClick = true; + }) + .click(); + using button = await frame.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('clicked'); + expect(willClick).toBe(true); + }); + }); + + describe('Locator.hover', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onmouseenter="this.innerText = 'hovered';">test</button> + `); + let hovered = false; + await page + .locator('button') + .on(LocatorEvent.Action, () => { + hovered = true; + }) + .hover(); + using button = await page.$('button'); + const text = await button?.evaluate(el => { + return el.innerText; + }); + expect(text).toBe('hovered'); + expect(hovered).toBe(true); + }); + }); + + describe('Locator.scroll', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <div style="height: 500px; width: 500px; overflow: scroll;"> + <div style="height: 1000px; width: 1000px;">test</div> + </div> + `); + let scrolled = false; + await page + .locator('div') + .on(LocatorEvent.Action, () => { + scrolled = true; + }) + .scroll({ + scrollTop: 500, + scrollLeft: 500, + }); + using scrollable = await page.$('div'); + const scroll = await scrollable?.evaluate(el => { + return el.scrollTop + ' ' + el.scrollLeft; + }); + expect(scroll).toBe('500 500'); + expect(scrolled).toBe(true); + }); + }); + + describe('Locator.fill', function () { + it('should work for textarea', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <textarea></textarea> + `); + let filled = false; + await page + .locator('textarea') + .on(LocatorEvent.Action, () => { + filled = true; + }) + .fill('test'); + expect( + await page.evaluate(() => { + return document.querySelector('textarea')?.value === 'test'; + }) + ).toBe(true); + expect(filled).toBe(true); + }); + + it('should work for selects', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <select> + <option value="value1">Option 1</option> + <option value="value2">Option 2</option> + <select> + `); + let filled = false; + await page + .locator('select') + .on(LocatorEvent.Action, () => { + filled = true; + }) + .fill('value2'); + expect( + await page.evaluate(() => { + return document.querySelector('select')?.value === 'value2'; + }) + ).toBe(true); + expect(filled).toBe(true); + }); + + it('should work for inputs', async () => { + const {page} = await getTestState(); + await page.setContent(` + <input> + `); + await page.locator('input').fill('test'); + expect( + await page.evaluate(() => { + return document.querySelector('input')?.value === 'test'; + }) + ).toBe(true); + }); + + it('should work if the input becomes enabled later', async () => { + const {page} = await getTestState(); + + await page.setContent(` + <input disabled> + `); + using input = await page.$('input'); + const result = page.locator('input').fill('test'); + expect( + await input?.evaluate(el => { + return el.value; + }) + ).toBe(''); + await input?.evaluate(el => { + el.disabled = false; + }); + await result; + expect( + await input?.evaluate(el => { + return el.value; + }) + ).toBe('test'); + }); + + it('should work for contenteditable', async () => { + const {page} = await getTestState(); + await page.setContent(` + <div contenteditable="true"> + `); + await page.locator('div').fill('test'); + expect( + await page.evaluate(() => { + return document.querySelector('div')?.innerText === 'test'; + }) + ).toBe(true); + }); + + it('should work for pre-filled inputs', async () => { + const {page} = await getTestState(); + await page.setContent(` + <input value="te"> + `); + await page.locator('input').fill('test'); + expect( + await page.evaluate(() => { + return document.querySelector('input')?.value === 'test'; + }) + ).toBe(true); + }); + + it('should override pre-filled inputs', async () => { + const {page} = await getTestState(); + await page.setContent(` + <input value="wrong prefix"> + `); + await page.locator('input').fill('test'); + expect( + await page.evaluate(() => { + return document.querySelector('input')?.value === 'test'; + }) + ).toBe(true); + }); + + it('should work for non-text inputs', async () => { + const {page} = await getTestState(); + await page.setContent(` + <input type="color"> + `); + await page.locator('input').fill('#333333'); + expect( + await page.evaluate(() => { + return document.querySelector('input')?.value === '#333333'; + }) + ).toBe(true); + }); + }); + + describe('Locator.race', () => { + it('races multiple locators', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button onclick="window.count++;">test</button> + `); + await page.evaluate(() => { + // @ts-expect-error different context. + window.count = 0; + }); + await Locator.race([ + page.locator('button'), + page.locator('button'), + ]).click(); + const count = await page.evaluate(() => { + // @ts-expect-error different context. + return globalThis.count; + }); + expect(count).toBe(1); + }); + + it('can be aborted', async () => { + const {page} = await getTestState(); + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + <button style="display: none;" onclick="this.innerText = 'clicked';">test</button> + `); + const abortController = new AbortController(); + const result = Locator.race([ + page.locator('button'), + page.locator('button'), + ]) + .setTimeout(5000) + .click({ + signal: abortController.signal, + }); + clock.tick(2000); + abortController.abort(); + await expect(result).rejects.toThrow(/aborted/); + } finally { + clock.restore(); + } + }); + + it('should time out when all locators do not match', async () => { + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + const {page} = await getTestState(); + await page.setContent(`<button>test</button>`); + const result = Locator.race([ + page.locator('not-found'), + page.locator('not-found'), + ]) + .setTimeout(5000) + .click(); + clock.tick(5100); + await expect(result).rejects.toEqual( + new TimeoutError('Timed out after waiting 5000ms') + ); + } finally { + clock.restore(); + } + }); + + it('should not time out when one of the locators matches', async () => { + const {page} = await getTestState(); + await page.setContent(`<button>test</button>`); + const result = Locator.race([ + page.locator('not-found'), + page.locator('button'), + ]).click(); + await expect(result).resolves.toEqual(undefined); + }); + }); + + describe('Locator.prototype.map', () => { + it('should work', async () => { + const {page} = await getTestState(); + await page.setContent(`<div>test</div>`); + await expect( + page + .locator('::-p-text(test)') + .map(element => { + return element.getAttribute('clickable'); + }) + .wait() + ).resolves.toEqual(null); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect( + page + .locator('::-p-text(test)') + .map(element => { + return element.getAttribute('clickable'); + }) + .wait() + ).resolves.toEqual('true'); + }); + it('should work with throws', async () => { + const {page} = await getTestState(); + await page.setContent(`<div>test</div>`); + const result = page + .locator('::-p-text(test)') + .map(element => { + const clickable = element.getAttribute('clickable'); + if (!clickable) { + throw new Error('Missing `clickable` as an attribute'); + } + return clickable; + }) + .wait(); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect(result).resolves.toEqual('true'); + }); + it('should work with expect', async () => { + const {page} = await getTestState(); + await page.setContent(`<div>test</div>`); + const result = page + .locator('::-p-text(test)') + .filter(element => { + return element.getAttribute('clickable') !== null; + }) + .map(element => { + return element.getAttribute('clickable'); + }) + .wait(); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + await expect(result).resolves.toEqual('true'); + }); + }); + + describe('Locator.prototype.filter', () => { + it('should resolve as soon as the predicate matches', async () => { + const clock = sinon.useFakeTimers({ + shouldClearNativeTimers: true, + shouldAdvanceTime: true, + }); + try { + const {page} = await getTestState(); + await page.setContent(`<div>test</div>`); + const result = page + .locator('::-p-text(test)') + .setTimeout(5000) + .filter(async element => { + return element.getAttribute('clickable') === 'true'; + }) + .filter(element => { + return element.getAttribute('clickable') === 'true'; + }) + .hover(); + clock.tick(2000); + await page.evaluate(() => { + document.querySelector('div')?.setAttribute('clickable', 'true'); + }); + clock.restore(); + await expect(result).resolves.toEqual(undefined); + } finally { + clock.restore(); + } + }); + }); + + describe('Locator.prototype.wait', () => { + it('should work', async () => { + const {page} = await getTestState(); + void page.setContent(` + <script> + setTimeout(() => { + const element = document.createElement("div"); + element.innerText = "test2" + document.body.append(element); + }, 50); + </script> + `); + // This shouldn't throw. + await page.locator('div').wait(); + }); + }); + + describe('Locator.prototype.waitHandle', () => { + it('should work', async () => { + const {page} = await getTestState(); + void page.setContent(` + <script> + setTimeout(() => { + const element = document.createElement("div"); + element.innerText = "test2" + document.body.append(element); + }, 50); + </script> + `); + await expect(page.locator('div').waitHandle()).resolves.toBeDefined(); + }); + }); + + describe('Locator.prototype.clone', () => { + it('should work', async () => { + const {page} = await getTestState(); + const locator = page.locator('div'); + const clone = locator.clone(); + expect(locator).not.toStrictEqual(clone); + }); + it('should work internally with delegated locators', async () => { + const {page} = await getTestState(); + const locator = page.locator('div'); + const delegatedLocators = [ + locator.map(div => { + return div.textContent; + }), + locator.filter(div => { + return div.textContent?.length === 0; + }), + ]; + for (let delegatedLocator of delegatedLocators) { + delegatedLocator = delegatedLocator.setTimeout(500); + expect(delegatedLocator.timeout).not.toStrictEqual(locator.timeout); + } + }); + }); + + describe('FunctionLocator', () => { + it('should work', async () => { + const {page} = await getTestState(); + const result = page + .locator(() => { + return new Promise<boolean>(resolve => { + return setTimeout(() => { + return resolve(true); + }, 100); + }); + }) + .wait(); + await expect(result).resolves.toEqual(true); + }); + it('should work with actions', async () => { + const {page} = await getTestState(); + await page.setContent(`<div onclick="window.clicked = true">test</div>`); + await page + .locator(() => { + return document.getElementsByTagName('div')[0] as HTMLDivElement; + }) + .click(); + await expect( + page.evaluate(() => { + return (window as unknown as {clicked: boolean}).clicked; + }) + ).resolves.toEqual(true); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/mocha-utils.ts b/remote/test/puppeteer/test/src/mocha-utils.ts new file mode 100644 index 0000000000..3fff9c9930 --- /dev/null +++ b/remote/test/puppeteer/test/src/mocha-utils.ts @@ -0,0 +1,507 @@ +/** + * @license + * Copyright 2020 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; + +import {TestServer} from '@pptr/testserver'; +import type {Protocol} from 'devtools-protocol'; +import expect from 'expect'; +import type * as MochaBase from 'mocha'; +import puppeteer from 'puppeteer/lib/cjs/puppeteer/puppeteer.js'; +import type {Browser} from 'puppeteer-core/internal/api/Browser.js'; +import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; +import type { + PuppeteerLaunchOptions, + PuppeteerNode, +} from 'puppeteer-core/internal/node/PuppeteerNode.js'; +import {rmSync} from 'puppeteer-core/internal/node/util/fs.js'; +import {Deferred} from 'puppeteer-core/internal/util/Deferred.js'; +import {isErrorLike} from 'puppeteer-core/internal/util/ErrorLike.js'; +import sinon from 'sinon'; + +import {extendExpectWithToBeGolden} from './utils.js'; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Mocha { + export interface SuiteFunction { + /** + * Use it if you want to capture debug logs for a specitic test suite in CI. + * This describe function enables capturing of debug logs and would print them + * only if a test fails to reduce the amount of output. + */ + withDebugLogs: ( + description: string, + body: (this: MochaBase.Suite) => void + ) => void; + } + export interface TestFunction { + /* + * Use to rerun the test and capture logs for the failed attempts + * that way we don't push all the logs making it easier to read. + */ + deflake: ( + repeats: number, + title: string, + fn: MochaBase.AsyncFunc + ) => void; + /* + * Use to rerun a single test and capture logs for the failed attempts + */ + deflakeOnly: ( + repeats: number, + title: string, + fn: MochaBase.AsyncFunc + ) => void; + } + } +} + +const product = + process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'chrome'; + +const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase() as + | 'true' + | 'false' + | 'new'; +export const isHeadless = headless === 'true' || headless === 'new'; +const isFirefox = product === 'firefox'; +const isChrome = product === 'chrome'; +const protocol = (process.env['PUPPETEER_PROTOCOL'] || 'cdp') as + | 'cdp' + | 'webDriverBiDi'; + +let extraLaunchOptions = {}; +try { + extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}'); +} catch (error) { + if (isErrorLike(error)) { + console.warn( + `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` + ); + } else { + throw error; + } +} + +const defaultBrowserOptions = Object.assign( + { + handleSIGINT: true, + executablePath: process.env['BINARY'], + headless: headless === 'new' ? ('new' as const) : isHeadless, + dumpio: !!process.env['DUMPIO'], + protocol, + }, + extraLaunchOptions +); + +if (defaultBrowserOptions.executablePath) { + console.warn( + `WARN: running ${product} tests with ${defaultBrowserOptions.executablePath}` + ); +} else { + const executablePath = puppeteer.executablePath(); + if (!fs.existsSync(executablePath)) { + throw new Error( + `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` + ); + } +} + +const processVariables: { + product: string; + headless: 'true' | 'false' | 'new'; + isHeadless: boolean; + isFirefox: boolean; + isChrome: boolean; + protocol: 'cdp' | 'webDriverBiDi'; + defaultBrowserOptions: PuppeteerLaunchOptions; +} = { + product, + headless, + isHeadless, + isFirefox, + isChrome, + protocol, + defaultBrowserOptions, +}; + +const setupServer = async () => { + const assetsPath = path.join(__dirname, '../assets'); + const cachedPath = path.join(__dirname, '../assets', 'cached'); + + const server = await TestServer.create(assetsPath); + const port = server.port; + server.enableHTTPCache(cachedPath); + server.PORT = port; + server.PREFIX = `http://localhost:${port}`; + server.CROSS_PROCESS_PREFIX = `http://127.0.0.1:${port}`; + server.EMPTY_PAGE = `http://localhost:${port}/empty.html`; + + const httpsServer = await TestServer.createHTTPS(assetsPath); + const httpsPort = httpsServer.port; + httpsServer.enableHTTPCache(cachedPath); + httpsServer.PORT = httpsPort; + httpsServer.PREFIX = `https://localhost:${httpsPort}`; + httpsServer.CROSS_PROCESS_PREFIX = `https://127.0.0.1:${httpsPort}`; + httpsServer.EMPTY_PAGE = `https://localhost:${httpsPort}/empty.html`; + + return {server, httpsServer}; +}; + +export const setupTestBrowserHooks = (): void => { + before(async function () { + try { + if (!state.browser) { + state.browser = await puppeteer.launch({ + ...processVariables.defaultBrowserOptions, + timeout: this.timeout() - 1_000, + }); + } + } catch (error) { + console.error(error); + // Intentionally empty as `getTestState` will throw + // if browser is not found + } + }); + + after(() => { + if (typeof gc !== 'undefined') { + gc(); + const memory = process.memoryUsage(); + console.log('Memory stats:'); + for (const key of Object.keys(memory)) { + console.log( + key, + // @ts-expect-error TS cannot the key type. + `${Math.round(((memory[key] / 1024 / 1024) * 100) / 100)} MB` + ); + } + } + }); +}; + +export const getTestState = async ( + options: { + skipLaunch?: boolean; + skipContextCreation?: boolean; + } = {} +): Promise<PuppeteerTestState> => { + const {skipLaunch = false, skipContextCreation = false} = options; + + state.defaultBrowserOptions = JSON.parse( + JSON.stringify(processVariables.defaultBrowserOptions) + ); + + state.server?.reset(); + state.httpsServer?.reset(); + + if (skipLaunch) { + return state as PuppeteerTestState; + } + + if (!state.browser) { + throw new Error('Browser was not set-up in time!'); + } + + if (state.context) { + await state.context.close(); + state.context = undefined; + state.page = undefined; + } + + if (!skipContextCreation) { + state.context = await state.browser!.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + } + return state as PuppeteerTestState; +}; + +const setupGoldenAssertions = (): void => { + const suffix = processVariables.product.toLowerCase(); + const GOLDEN_DIR = path.join(__dirname, `../golden-${suffix}`); + const OUTPUT_DIR = path.join(__dirname, `../output-${suffix}`); + if (fs.existsSync(OUTPUT_DIR)) { + rmSync(OUTPUT_DIR); + } + extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); +}; + +setupGoldenAssertions(); + +export interface PuppeteerTestState { + browser: Browser; + context: BrowserContext; + page: Page; + puppeteer: PuppeteerNode; + defaultBrowserOptions: PuppeteerLaunchOptions; + server: TestServer; + httpsServer: TestServer; + isFirefox: boolean; + isChrome: boolean; + isHeadless: boolean; + headless: 'true' | 'false' | 'new'; + puppeteerPath: string; +} +const state: Partial<PuppeteerTestState> = {}; + +if ( + process.env['MOCHA_WORKER_ID'] === undefined || + process.env['MOCHA_WORKER_ID'] === '0' +) { + console.log( + `Running unit tests with: + -> product: ${processVariables.product} + -> binary: ${ + processVariables.defaultBrowserOptions.executablePath || + path.relative(process.cwd(), puppeteer.executablePath()) + } + -> mode: ${ + processVariables.isHeadless + ? processVariables.headless === 'new' + ? '--headless=new' + : '--headless' + : 'headful' + }` + ); +} + +const browserNotClosedError = new Error( + 'A manually launched browser was not closed!' +); + +export const mochaHooks = { + async beforeAll(): Promise<void> { + async function setUpDefaultState() { + const {server, httpsServer} = await setupServer(); + + state.puppeteer = puppeteer; + state.server = server; + state.httpsServer = httpsServer; + state.isFirefox = processVariables.isFirefox; + state.isChrome = processVariables.isChrome; + state.isHeadless = processVariables.isHeadless; + state.headless = processVariables.headless; + state.puppeteerPath = path.resolve( + path.join(__dirname, '..', '..', 'packages', 'puppeteer') + ); + } + + try { + await Deferred.race([ + setUpDefaultState(), + Deferred.create({ + message: `Failed in after Hook`, + timeout: (this as any).timeout() - 1000, + }), + ]); + } catch {} + }, + + async afterAll(): Promise<void> { + (this as any).timeout(0); + const lastTestFile = (this as any)?.test?.parent?.suites?.[0]?.file + ?.split('/') + ?.at(-1); + try { + await Promise.all([ + state.server?.stop(), + state.httpsServer?.stop(), + state.browser?.close(), + ]); + } catch (error) { + throw new Error( + `Closing defaults (HTTP TestServer, HTTPS TestServer, Browser ) failed in ${lastTestFile}}` + ); + } + if (browserCleanupsAfterAll.length > 0) { + await closeLaunched(browserCleanupsAfterAll)(); + throw new Error(`Browser was not closed in ${lastTestFile}`); + } + }, + + async afterEach(): Promise<void> { + if (browserCleanups.length > 0) { + (this as any).test.error(browserNotClosedError); + await Deferred.race([ + closeLaunched(browserCleanups)(), + Deferred.create({ + message: `Failed in after Hook`, + timeout: (this as any).timeout() - 1000, + }), + ]); + } + sinon.restore(); + }, +}; + +declare module 'expect' { + interface Matchers<R> { + atLeastOneToContain(expected: string[]): R; + } +} + +expect.extend({ + atLeastOneToContain: (actual: string, expected: string[]) => { + for (const test of expected) { + try { + expect(actual).toContain(test); + return { + pass: true, + message: () => { + return ''; + }, + }; + } catch (err) {} + } + + return { + pass: false, + message: () => { + return `"${actual}" didn't contain any of the strings ${JSON.stringify( + expected + )}`; + }, + }; + }, +}); + +export const expectCookieEquals = async ( + cookies: Protocol.Network.Cookie[], + expectedCookies: Array<Partial<Protocol.Network.Cookie>> +): Promise<void> => { + if (!processVariables.isChrome) { + // Only keep standard properties when testing on a browser other than Chrome. + expectedCookies = expectedCookies.map(cookie => { + return { + domain: cookie.domain, + expires: cookie.expires, + httpOnly: cookie.httpOnly, + name: cookie.name, + path: cookie.path, + secure: cookie.secure, + session: cookie.session, + size: cookie.size, + value: cookie.value, + }; + }); + } + + expect(cookies).toHaveLength(expectedCookies.length); + for (let i = 0; i < cookies.length; i++) { + expect(cookies[i]).toMatchObject(expectedCookies[i]!); + } +}; + +export const shortWaitForArrayToHaveAtLeastNElements = async ( + data: unknown[], + minLength: number, + attempts = 3, + timeout = 50 +): Promise<void> => { + for (let i = 0; i < attempts; i++) { + if (data.length >= minLength) { + break; + } + await new Promise(resolve => { + return setTimeout(resolve, timeout); + }); + } +}; + +export const createTimeout = <T>( + n: number, + value?: T +): Promise<T | undefined> => { + return new Promise(resolve => { + setTimeout(() => { + return resolve(value); + }, n); + }); +}; + +const browserCleanupsAfterAll: Array<() => Promise<void>> = []; +const browserCleanups: Array<() => Promise<void>> = []; + +const closeLaunched = (storage: Array<() => Promise<void>>) => { + return async () => { + let cleanup = storage.pop(); + try { + while (cleanup) { + await cleanup(); + cleanup = storage.pop(); + } + } catch (error) { + // If the browser was closed by other means, swallow the error + // and mark the browser as closed. + if ((error as Error)?.message.includes('Connection closed')) { + storage.splice(0, storage.length); + return; + } + + throw error; + } + }; +}; + +export const launch = async ( + launchOptions: Readonly<PuppeteerLaunchOptions>, + options: { + after?: 'each' | 'all'; + createContext?: boolean; + createPage?: boolean; + } = {} +): Promise< + PuppeteerTestState & { + close: () => Promise<void>; + } +> => { + const {after = 'each', createContext = true, createPage = true} = options; + const initState = await getTestState({ + skipLaunch: true, + }); + const cleanupStorage = + after === 'each' ? browserCleanups : browserCleanupsAfterAll; + try { + const browser = await puppeteer.launch({ + ...initState.defaultBrowserOptions, + ...launchOptions, + }); + cleanupStorage.push(() => { + return browser.close(); + }); + + let context: BrowserContext; + let page: Page; + if (createContext) { + context = await browser.createIncognitoBrowserContext(); + cleanupStorage.push(() => { + return context.close(); + }); + + if (createPage) { + page = await context.newPage(); + cleanupStorage.push(() => { + return page.close(); + }); + } + } + + return { + ...initState, + browser, + context: context!, + page: page!, + close: closeLaunched(cleanupStorage), + }; + } catch (error) { + await closeLaunched(cleanupStorage)(); + + throw error; + } +}; diff --git a/remote/test/puppeteer/test/src/mouse.spec.ts b/remote/test/puppeteer/test/src/mouse.spec.ts new file mode 100644 index 0000000000..69229eb147 --- /dev/null +++ b/remote/test/puppeteer/test/src/mouse.spec.ts @@ -0,0 +1,472 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import os from 'os'; + +import expect from 'expect'; +import {MouseButton} from 'puppeteer-core/internal/api/Input.js'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; +import type {KeyInput} from 'puppeteer-core/internal/common/USKeyboardLayout.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +interface ClickData { + type: string; + detail: number; + clientX: number; + clientY: number; + isTrusted: boolean; + button: number; + buttons: number; +} + +interface Dimensions { + x: number; + y: number; + width: number; + height: number; +} + +function dimensions(): Dimensions { + const rect = document.querySelector('textarea')!.getBoundingClientRect(); + return { + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; +} + +describe('Mouse', function () { + setupTestBrowserHooks(); + + it('should click the document', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + (globalThis as any).clickPromise = new Promise(resolve => { + document.addEventListener('click', event => { + resolve({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button, + }); + }); + }); + }); + await page.mouse.click(50, 60); + const event = await page.evaluate(() => { + return (globalThis as any).clickPromise; + }); + expect(event.type).toBe('click'); + expect(event.detail).toBe(1); + expect(event.clientX).toBe(50); + expect(event.clientY).toBe(60); + expect(event.isTrusted).toBe(true); + expect(event.button).toBe(0); + }); + it('should resize the textarea', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/textarea.html'); + const {x, y, width, height} = await page.evaluate(dimensions); + const mouse = page.mouse; + await mouse.move(x + width - 4, y + height - 4); + await mouse.down(); + await mouse.move(x + width + 100, y + height + 100); + await mouse.up(); + const newDimensions = await page.evaluate(dimensions); + expect(newDimensions.width).toBe(Math.round(width + 104)); + expect(newDimensions.height).toBe(Math.round(height + 104)); + }); + it('should select the text with mouse', async () => { + const {page, server} = await getTestState(); + + const text = + "This is the text that we are going to try to select. Let's see how it goes."; + + await page.goto(`${server.PREFIX}/input/textarea.html`); + await page.focus('textarea'); + await page.keyboard.type(text); + using handle = await page + .locator('textarea') + .filterHandle(async element => { + return await element.evaluate((element, text) => { + return element.value === text; + }, text); + }) + .waitHandle(); + const {x, y} = await page.evaluate(dimensions); + await page.mouse.move(x + 2, y + 2); + await page.mouse.down(); + await page.mouse.move(100, 100); + await page.mouse.up(); + expect( + await handle.evaluate(element => { + return element.value.substring( + element.selectionStart, + element.selectionEnd + ); + }) + ).toBe(text); + }); + it('should trigger hover state', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.hover('#button-6'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-6'); + await page.hover('#button-2'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-2'); + await page.hover('#button-91'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-91'); + }); + it('should trigger hover state with removed window.Node', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => { + // @ts-expect-error Expected. + return delete window.Node; + }); + await page.hover('#button-6'); + expect( + await page.evaluate(() => { + return document.querySelector('button:hover')!.id; + }) + ).toBe('button-6'); + }); + it('should set modifier keys on click', async () => { + const {page, server, isFirefox} = await getTestState(); + + await page.goto(server.PREFIX + '/input/scrollable.html'); + await page.evaluate(() => { + return document.querySelector('#button-3')!.addEventListener( + 'mousedown', + e => { + return ((globalThis as any).lastEvent = e); + }, + true + ); + }); + const modifiers = new Map<KeyInput, string>([ + ['Shift', 'shiftKey'], + ['Control', 'ctrlKey'], + ['Alt', 'altKey'], + ['Meta', 'metaKey'], + ]); + // In Firefox, the Meta modifier only exists on Mac + if (isFirefox && os.platform() !== 'darwin') { + modifiers.delete('Meta'); + } + for (const [modifier, key] of modifiers) { + await page.keyboard.down(modifier); + await page.click('#button-3'); + if ( + !(await page.evaluate((mod: string) => { + return (globalThis as any).lastEvent[mod]; + }, key)) + ) { + throw new Error(key + ' should be true'); + } + await page.keyboard.up(modifier); + } + await page.click('#button-3'); + for (const [modifier, key] of modifiers) { + if ( + await page.evaluate((mod: string) => { + return (globalThis as any).lastEvent[mod]; + }, key) + ) { + throw new Error(modifiers.get(modifier) + ' should be false'); + } + } + }); + it('should send mouse wheel events', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/wheel.html'); + using elem = (await page.$('div'))!; + const boundingBoxBefore = (await elem.boundingBox())!; + expect(boundingBoxBefore).toMatchObject({ + width: 115, + height: 115, + }); + + await page.mouse.move( + boundingBoxBefore.x + boundingBoxBefore.width / 2, + boundingBoxBefore.y + boundingBoxBefore.height / 2 + ); + + await page.mouse.wheel({deltaY: -100}); + const boundingBoxAfter = await elem.boundingBox(); + expect(boundingBoxAfter).toMatchObject({ + width: 230, + height: 230, + }); + }); + it('should tween mouse movement', async () => { + const {page} = await getTestState(); + + await page.mouse.move(100, 100); + await page.evaluate(() => { + (globalThis as any).result = []; + document.addEventListener('mousemove', event => { + (globalThis as any).result.push([event.clientX, event.clientY]); + }); + }); + await page.mouse.move(200, 300, {steps: 5}); + expect(await page.evaluate('result')).toEqual([ + [120, 140], + [140, 180], + [160, 220], + [180, 260], + [200, 300], + ]); + }); + // @see https://crbug.com/929806 + it('should work with mobile viewports and cross process navigations', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setViewport({width: 360, height: 640, isMobile: true}); + await page.goto(server.CROSS_PROCESS_PREFIX + '/mobile.html'); + await page.evaluate(() => { + document.addEventListener('click', event => { + (globalThis as any).result = {x: event.clientX, y: event.clientY}; + }); + }); + + await page.mouse.click(30, 40); + + expect(await page.evaluate('result')).toEqual({x: 30, y: 40}); + }); + it('should not throw if buttons are pressed twice', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.mouse.down(); + await page.mouse.down(); + }); + + interface AddMouseDataListenersOptions { + includeMove?: boolean; + } + + const addMouseDataListeners = ( + page: Page, + options: AddMouseDataListenersOptions = {} + ) => { + return page.evaluate(({includeMove}) => { + const clicks: ClickData[] = []; + const mouseEventListener = (event: MouseEvent) => { + clicks.push({ + type: event.type, + detail: event.detail, + clientX: event.clientX, + clientY: event.clientY, + isTrusted: event.isTrusted, + button: event.button, + buttons: event.buttons, + }); + }; + document.addEventListener('mousedown', mouseEventListener); + if (includeMove) { + document.addEventListener('mousemove', mouseEventListener); + } + document.addEventListener('mouseup', mouseEventListener); + document.addEventListener('click', mouseEventListener); + document.addEventListener('auxclick', mouseEventListener); + (window as unknown as {clicks: ClickData[]}).clicks = clicks; + }, options); + }; + + it('should not throw if clicking in parallel', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await addMouseDataListeners(page); + + await Promise.all([page.mouse.click(0, 5), page.mouse.click(6, 10)]); + + const data = await page.evaluate(() => { + return (window as unknown as {clicks: ClickData[]}).clicks; + }); + const commonAttrs = { + isTrusted: true, + detail: 1, + clientY: 5, + clientX: 0, + button: 0, + }; + expect(data.splice(0, 3)).toMatchObject({ + 0: { + type: 'mousedown', + buttons: 1, + ...commonAttrs, + }, + 1: { + type: 'mouseup', + buttons: 0, + ...commonAttrs, + }, + 2: { + type: 'click', + buttons: 0, + ...commonAttrs, + }, + }); + Object.assign(commonAttrs, { + clientX: 6, + clientY: 10, + }); + expect(data).toMatchObject({ + 0: { + type: 'mousedown', + buttons: 1, + ...commonAttrs, + }, + 1: { + type: 'mouseup', + buttons: 0, + ...commonAttrs, + }, + 2: { + type: 'click', + buttons: 0, + ...commonAttrs, + }, + }); + }); + + it('should reset properly', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.mouse.move(5, 5); + await Promise.all([ + page.mouse.down({button: MouseButton.Left}), + page.mouse.down({button: MouseButton.Middle}), + page.mouse.down({button: MouseButton.Right}), + ]); + + await addMouseDataListeners(page, {includeMove: true}); + await page.mouse.reset(); + + const data = await page.evaluate(() => { + return (window as unknown as {clicks: ClickData[]}).clicks; + }); + const commonAttrs = { + isTrusted: true, + clientY: 5, + clientX: 5, + }; + + expect(data.slice(0, 2)).toMatchObject([ + { + ...commonAttrs, + button: 2, + buttons: 5, + detail: 1, + type: 'mouseup', + }, + { + ...commonAttrs, + button: 2, + buttons: 5, + detail: 1, + type: 'auxclick', + }, + ]); + // TODO(crbug/1485040): This should align with the firefox implementation. + if (isChrome) { + expect(data.slice(2)).toMatchObject([ + { + ...commonAttrs, + button: 1, + buttons: 1, + detail: 0, + type: 'mouseup', + }, + { + ...commonAttrs, + button: 0, + buttons: 0, + detail: 0, + type: 'mouseup', + }, + ]); + return; + } + expect(data.slice(2)).toMatchObject([ + { + ...commonAttrs, + button: 1, + buttons: 1, + detail: 1, + type: 'mouseup', + }, + { + ...commonAttrs, + button: 1, + buttons: 1, + detail: 1, + type: 'auxclick', + }, + { + ...commonAttrs, + button: 0, + buttons: 0, + detail: 1, + type: 'mouseup', + }, + { + ...commonAttrs, + button: 0, + buttons: 0, + detail: 1, + type: 'click', + }, + ]); + }); + + it('should evaluate before mouse event', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.goto(server.CROSS_PROCESS_PREFIX + '/input/button.html'); + + using button = await page.waitForSelector('button'); + + const point = await button!.clickablePoint(); + + const result = page.evaluate(() => { + return new Promise(resolve => { + document + .querySelector('button') + ?.addEventListener('click', resolve, {once: true}); + }); + }); + await page.mouse.click(point?.x, point?.y); + await result; + }); +}); diff --git a/remote/test/puppeteer/test/src/navigation.spec.ts b/remote/test/puppeteer/test/src/navigation.spec.ts new file mode 100644 index 0000000000..1f3a51f58a --- /dev/null +++ b/remote/test/puppeteer/test/src/navigation.spec.ts @@ -0,0 +1,918 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {ServerResponse} from 'http'; + +import expect from 'expect'; +import {type Frame, TimeoutError} from 'puppeteer'; +import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; +import {Deferred} from 'puppeteer-core/internal/util/Deferred.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('navigation', function () { + setupTestBrowserHooks(); + + describe('Page.goto', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with anchor navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goto(server.EMPTY_PAGE + '#foo'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foo'); + await page.goto(server.EMPTY_PAGE + '#bar'); + expect(page.url()).toBe(server.EMPTY_PAGE + '#bar'); + }); + it('should work with redirects', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + await page.goto(server.PREFIX + '/redirect/1.html'); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + it('should navigate to about:blank', async () => { + const {page} = await getTestState(); + + const response = await page.goto('about:blank'); + expect(response).toBe(null); + }); + it('should return response when page changes its URL after load', async () => { + const {page, server} = await getTestState(); + + const response = await page.goto(server.PREFIX + '/historyapi.html'); + expect(response!.status()).toBe(200); + }); + it('should work with subframes return 204', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/frames/frame.html', (_req, res) => { + res.statusCode = 204; + res.end(); + }); + let error!: Error; + await page + .goto(server.PREFIX + '/frames/one-frame.html') + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should fail when server returns 204', async () => { + const {page, server, isChrome} = await getTestState(); + + server.setRoute('/empty.html', (_req, res) => { + res.statusCode = 204; + res.end(); + }); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).not.toBe(null); + if (isChrome) { + expect(error.message).toContain('net::ERR_ABORTED'); + } else { + expect(error.message).toContain('NS_BINDING_ABORTED'); + } + }); + it('should navigate to empty page with domcontentloaded', async () => { + const {page, server} = await getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'domcontentloaded', + }); + expect(response!.status()).toBe(200); + }); + it('should work when page calls history API in beforeunload', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + window.addEventListener( + 'beforeunload', + () => { + return history.replaceState(null, 'initial', window.location.href); + }, + false + ); + }); + const response = await page.goto(server.PREFIX + '/grid.html'); + expect(response!.status()).toBe(200); + }); + it('should navigate to empty page with networkidle0', async () => { + const {page, server} = await getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle0', + }); + expect(response!.status()).toBe(200); + }); + it('should navigate to page with iframe and networkidle0', async () => { + const {page, server} = await getTestState(); + + const response = await page.goto( + server.PREFIX + '/frames/one-frame.html', + { + waitUntil: 'networkidle0', + } + ); + expect(response!.status()).toBe(200); + }); + it('should navigate to empty page with networkidle2', async () => { + const {page, server} = await getTestState(); + + const response = await page.goto(server.EMPTY_PAGE, { + waitUntil: 'networkidle2', + }); + expect(response!.status()).toBe(200); + }); + it('should fail when navigating to bad url', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page.goto('asdfasdf').catch(error_ => { + return (error = error_); + }); + + expect(error.message).atLeastOneToContain([ + 'Cannot navigate to invalid URL', // Firefox WebDriver BiDi. + 'invalid argument', // Others. + ]); + }); + + const EXPECTED_SSL_CERT_MESSAGE_REGEX = + /net::ERR_CERT_INVALID|net::ERR_CERT_AUTHORITY_INVALID/; + + it('should fail when navigating to bad SSL', async () => { + const {page, httpsServer, isChrome} = await getTestState(); + + // Make sure that network events do not emit 'undefined'. + // @see https://crbug.com/750469 + const requests: string[] = []; + page.on('request', () => { + return requests.push('request'); + }); + page.on('requestfinished', () => { + return requests.push('requestfinished'); + }); + page.on('requestfailed', () => { + return requests.push('requestfailed'); + }); + + let error!: Error; + await page.goto(httpsServer.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX); + } else { + expect(error.message).toContain('SSL_ERROR_UNKNOWN'); + } + + expect(requests).toHaveLength(2); + expect(requests[0]).toBe('request'); + expect(requests[1]).toBe('requestfailed'); + }); + it('should fail when navigating to bad SSL after redirects', async () => { + const {page, server, httpsServer, isChrome} = await getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/empty.html'); + let error!: Error; + await page.goto(httpsServer.PREFIX + '/redirect/1.html').catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toMatch(EXPECTED_SSL_CERT_MESSAGE_REGEX); + } else { + expect(error.message).atLeastOneToContain([ + 'MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT', // Firefox WebDriver BiDi. + 'SSL_ERROR_UNKNOWN ', // Others. + ]); + } + }); + it('should fail when main resources failed to load', async () => { + const {page, isChrome} = await getTestState(); + + let error!: Error; + await page + .goto('http://localhost:44123/non-existing-url') + .catch(error_ => { + return (error = error_); + }); + if (isChrome) { + expect(error.message).toContain('net::ERR_CONNECTION_REFUSED'); + } else { + expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED'); + } + }); + it('should fail when exceeding maximum navigation timeout', async () => { + const {page, server} = await getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + await page + .goto(server.PREFIX + '/empty.html', {timeout: 1}) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should fail when exceeding default maximum navigation timeout', async () => { + const {page, server} = await getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should fail when exceeding default maximum timeout', async () => { + const {page, server} = await getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should prioritize default navigation timeout over default timeout', async () => { + const {page, server} = await getTestState(); + + // Hang for request to the empty.html + server.setRoute('/empty.html', () => {}); + let error!: Error; + page.setDefaultTimeout(0); + page.setDefaultNavigationTimeout(1); + await page.goto(server.PREFIX + '/empty.html').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Navigation timeout of 1 ms exceeded'); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should disable timeout when its set to 0', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + let loaded = false; + page.once('load', () => { + loaded = true; + }); + await page + .goto(server.PREFIX + '/grid.html', {timeout: 0, waitUntil: ['load']}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + expect(loaded).toBe(true); + }); + it('should work when navigating to valid url', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + }); + it('should work when navigating to data url', async () => { + const {page} = await getTestState(); + + const response = (await page.goto('data:text/html,hello'))!; + expect(response.ok()).toBe(true); + }); + it('should work when navigating to 404', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.PREFIX + '/not-found'))!; + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should not throw an error for a 404 response with an empty body', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/404-error', (_, res) => { + res.statusCode = 404; + res.end(); + }); + + const response = (await page.goto(server.PREFIX + '/404-error'))!; + expect(response.ok()).toBe(false); + expect(response.status()).toBe(404); + }); + it('should not throw an error for a 500 response with an empty body', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/500-error', (_, res) => { + res.statusCode = 500; + res.end(); + }); + + const response = (await page.goto(server.PREFIX + '/500-error'))!; + expect(response.ok()).toBe(false); + expect(response.status()).toBe(500); + }); + it('should return last response in redirect chain', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/redirect/1.html', '/redirect/2.html'); + server.setRedirect('/redirect/2.html', '/redirect/3.html'); + server.setRedirect('/redirect/3.html', server.EMPTY_PAGE); + const response = (await page.goto(server.PREFIX + '/redirect/1.html'))!; + expect(response.ok()).toBe(true); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should wait for network idle to succeed navigation', async () => { + const {page, server} = await getTestState(); + + let responses: ServerResponse[] = []; + // Hold on to a bunch of requests without answering. + server.setRoute('/fetch-request-a.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-b.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-c.js', (_req, res) => { + return responses.push(res); + }); + server.setRoute('/fetch-request-d.js', (_req, res) => { + return responses.push(res); + }); + const initialFetchResourcesRequested = Promise.all([ + server.waitForRequest('/fetch-request-a.js'), + server.waitForRequest('/fetch-request-b.js'), + server.waitForRequest('/fetch-request-c.js'), + ]).catch(() => { + // Ignore Error that arise from test server during hooks + }); + const secondFetchResourceRequested = server + .waitForRequest('/fetch-request-d.js') + .catch(() => { + // Ignore Error that arise from test server during hooks + }); + + // Track when the navigation gets completed. + let navigationFinished = false; + let navigationError: Error | undefined; + // Navigate to a page which loads immediately and then does a bunch of + // requests via javascript's fetch method. + const navigationPromise = page + .goto(server.PREFIX + '/networkidle.html', { + waitUntil: 'networkidle0', + }) + .then(response => { + navigationFinished = true; + return response; + }) + .catch(error => { + navigationError = error; + return null; + }); + + let afterNavigationError: Error | undefined; + const afterNavigationPromise = (async () => { + // Wait for the page's 'load' event. + await waitEvent(page, 'load'); + expect(navigationFinished).toBe(false); + + // Wait for the initial three resources to be requested. + await initialFetchResourcesRequested; + + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to initial requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + + // Reset responses array + responses = []; + + // Wait for the second round to be requested. + await secondFetchResourceRequested; + // Expect navigation still to be not finished. + expect(navigationFinished).toBe(false); + + // Respond to requests. + for (const response of responses) { + response.statusCode = 404; + response.end(`File not found`); + } + })().catch(error => { + afterNavigationError = error; + }); + + await Promise.race([navigationPromise, afterNavigationPromise]); + if (navigationError) { + throw navigationError; + } + await Promise.all([navigationPromise, afterNavigationPromise]); + if (afterNavigationError) { + throw afterNavigationError; + } + // Expect navigation to succeed. + expect(navigationFinished).toBeTruthy(); + expect((await navigationPromise)?.ok()).toBe(true); + }); + it('should not leak listeners during navigation', async function () { + this.timeout(25_000); + + const {page, server} = await getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (warning = w); + }; + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) { + await page.goto(server.EMPTY_PAGE); + } + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during bad navigation', async function () { + this.timeout(25_000); + + const {page} = await getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (warning = w); + }; + process.on('warning', warningHandler); + for (let i = 0; i < 20; ++i) { + await page.goto('asdf').catch(() => { + /* swallow navigation error */ + }); + } + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should not leak listeners during navigation of 11 pages', async function () { + this.timeout(25_000); + + const {context, server} = await getTestState(); + + let warning = null; + const warningHandler: NodeJS.WarningListener = w => { + return (warning = w); + }; + process.on('warning', warningHandler); + await Promise.all( + [...Array(20)].map(async () => { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.close(); + }) + ); + process.removeListener('warning', warningHandler); + expect(warning).toBe(null); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const {page} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = (await page.goto(dataURL))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!; + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with self requesting page', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.PREFIX + '/self-request.html'))!; + expect(response.status()).toBe(200); + expect(response.url()).toContain('self-request.html'); + }); + it('should fail when navigating and show the url at the error message', async () => { + const {page, httpsServer} = await getTestState(); + + const url = httpsServer.PREFIX + '/redirect/1.html'; + let error!: Error; + try { + await page.goto(url); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain(url); + }); + it('should send referer', async () => { + const {page, server} = await getTestState(); + + const requests = Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referer: 'http://google.com/', + }), + ]).catch(() => { + return []; + }); + + const [request1, request2] = await requests; + expect(request1.headers['referer']).toBe('http://google.com/'); + // Make sure subresources do not inherit referer. + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + + it('should send referer policy', async () => { + const {page, server} = await getTestState(); + + const [request1, request2] = await Promise.all([ + server.waitForRequest('/grid.html'), + server.waitForRequest('/digits/1.png'), + page.goto(server.PREFIX + '/grid.html', { + referrerPolicy: 'no-referer', + }), + ]).catch(() => { + return []; + }); + expect(request1.headers['referer']).toBeUndefined(); + expect(request2.headers['referer']).toBe(server.PREFIX + '/grid.html'); + }); + }); + + describe('Page.waitForNavigation', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.evaluate((url: string) => { + return (window.location.href = url); + }, server.PREFIX + '/grid.html'), + ]); + expect(response!.ok()).toBe(true); + expect(response!.url()).toContain('grid.html'); + }); + it('should work with both domcontentloaded and load', async () => { + const {page, server} = await getTestState(); + + let response!: ServerResponse; + server.setRoute('/one-style.css', (_req, res) => { + return (response = res); + }); + let error: Error | undefined; + let bothFired = false; + const navigationPromise = page + .goto(server.PREFIX + '/one-style.html') + .catch(_error => { + return (error = _error); + }); + const domContentLoadedPromise = page + .waitForNavigation({ + waitUntil: 'domcontentloaded', + }) + .catch(_error => { + return (error = _error); + }); + + const loadFiredPromise = page + .waitForNavigation({ + waitUntil: 'load', + }) + .then(() => { + return (bothFired = true); + }) + .catch(_error => { + return (error = _error); + }); + + await server.waitForRequest('/one-style.css').catch(() => {}); + await domContentLoadedPromise; + expect(bothFired).toBe(false); + response.end(); + await loadFiredPromise; + await navigationPromise; + expect(error).toBeUndefined(); + }); + it('should work with clicking on anchor links', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(`<a href='#foobar'>foobar</a>`); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.EMPTY_PAGE + '#foobar'); + }); + it('should work with history.pushState()', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:pushState()'>SPA</a> + <script> + function pushState() { history.pushState({}, '', 'wow.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/wow.html'); + }); + it('should work with history.replaceState()', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a onclick='javascript:replaceState()'>SPA</a> + <script> + function replaceState() { history.replaceState({}, '', '/replaced.html') } + </script> + `); + const [response] = await Promise.all([ + page.waitForNavigation(), + page.click('a'), + ]); + expect(response).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/replaced.html'); + }); + it('should work with DOM history.back()/history.forward()', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent(` + <a id=back onclick='javascript:goBack()'>back</a> + <a id=forward onclick='javascript:goForward()'>forward</a> + <script> + function goBack() { history.back(); } + function goForward() { history.forward(); } + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + </script> + `); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + const [backResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#back'), + ]); + expect(backResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + const [forwardResponse] = await Promise.all([ + page.waitForNavigation(), + page.click('a#forward'), + ]); + expect(forwardResponse).toBe(null); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + }); + it('should work when subframe issues window.stop()', async function () { + const {page, server} = await getTestState(); + + server.setRoute('/frames/style.css', () => {}); + let frame: Frame | undefined; + const eventPromises = Deferred.race([ + Promise.all([ + waitEvent(page, 'frameattached').then(_frame => { + return (frame = _frame); + }), + waitEvent(page, 'framenavigated', f => { + return f === frame; + }), + ]), + Deferred.create({ + message: `should work when subframe issues window.stop()`, + timeout: this.timeout() - 1000, + }), + ]); + const navigationPromise = page.goto( + server.PREFIX + '/frames/one-frame.html' + ); + try { + await eventPromises; + } catch (error) { + navigationPromise.catch(() => {}); + throw error; + } + await Promise.all([ + frame!.evaluate(() => { + return window.stop(); + }), + navigationPromise, + ]); + }); + }); + + describe('Page.goBack', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.goto(server.PREFIX + '/grid.html'); + + let response = (await page.goBack())!; + expect(response.ok()).toBe(true); + expect(response.url()).toContain(server.EMPTY_PAGE); + + response = (await page.goForward())!; + expect(response.ok()).toBe(true); + expect(response.url()).toContain('/grid.html'); + + response = (await page.goForward())!; + expect(response).toBe(null); + }); + it('should work with HistoryAPI', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + history.pushState({}, '', '/second.html'); + }); + expect(page.url()).toBe(server.PREFIX + '/second.html'); + + await page.goBack(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + await page.goBack(); + expect(page.url()).toBe(server.EMPTY_PAGE); + await page.goForward(); + expect(page.url()).toBe(server.PREFIX + '/first.html'); + }); + }); + + describe('Frame.goto', function () { + it('should navigate subframes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + expect(page.frames()[0]!.url()).toContain('/frames/one-frame.html'); + expect(page.frames()[1]!.url()).toContain('/frames/frame.html'); + + const response = (await page.frames()[1]!.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + expect(response.frame()).toBe(page.frames()[1]); + }); + it('should reject when frame detaches', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + + server.setRoute('/empty.html', () => {}); + const navigationPromise = page + .frames()[1]! + .goto(server.EMPTY_PAGE) + .catch(error_ => { + return error_; + }); + await server.waitForRequest('/empty.html').catch(() => {}); + + await page.$eval('iframe', frame => { + return frame.remove(); + }); + const error = await navigationPromise; + expect(error.message).atLeastOneToContain([ + 'Navigating frame was detached', + 'Frame detached', + 'Error: NS_BINDING_ABORTED', + 'net::ERR_ABORTED', + ]); + }); + it('should return matching responses', async () => { + const {page, server} = await getTestState(); + + // Disable cache: otherwise, the browser will cache similar requests. + await page.setCacheEnabled(false); + await page.goto(server.EMPTY_PAGE); + // Attach three frames. + const frames = await Promise.all([ + attachFrame(page, 'frame1', server.EMPTY_PAGE), + attachFrame(page, 'frame2', server.EMPTY_PAGE), + attachFrame(page, 'frame3', server.EMPTY_PAGE), + ]); + // Navigate all frames to the same URL. + const serverResponses: ServerResponse[] = []; + server.setRoute('/one-style.html', (_req, res) => { + return serverResponses.push(res); + }); + const navigations: Array<Promise<HTTPResponse | null>> = []; + for (let i = 0; i < 3; ++i) { + navigations.push(frames[i]!.goto(server.PREFIX + '/one-style.html')); + await server.waitForRequest('/one-style.html'); + } + // Respond from server out-of-order. + const serverResponseTexts = ['AAA', 'BBB', 'CCC']; + try { + for (const i of [1, 2, 0]) { + const response = await getResponse(i); + expect(response.frame()).toBe(frames[i]); + expect(await response.text()).toBe(serverResponseTexts[i]); + } + } catch (error) { + await Promise.all([getResponse(0), getResponse(1), getResponse(2)]); + throw error; + } + + async function getResponse(index: number) { + serverResponses[index]!.end(serverResponseTexts[index]); + return (await navigations[index])!; + } + }); + }); + + describe('Frame.waitForNavigation', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]!; + const [response] = await Promise.all([ + frame.waitForNavigation(), + frame.evaluate((url: string) => { + return (window.location.href = url); + }, server.PREFIX + '/grid.html'), + ]); + expect(response!.ok()).toBe(true); + expect(response!.url()).toContain('grid.html'); + expect(response!.frame()).toBe(frame); + expect(page.url()).toContain('/frames/one-frame.html'); + }); + it('should fail when frame detaches', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/one-frame.html'); + const frame = page.frames()[1]!; + + server.setRoute('/empty.html', () => {}); + let error!: Error; + const navigationPromise = frame.waitForNavigation().catch(error_ => { + return (error = error_); + }); + await Promise.all([ + server.waitForRequest('/empty.html'), + frame.evaluate(() => { + return ((window as any).location = '/empty.html'); + }), + ]); + await page.$eval('iframe', frame => { + return frame.remove(); + }); + await navigationPromise; + expect(error.message).atLeastOneToContain([ + 'Navigating frame was detached', + 'Frame detached', + ]); + }); + }); + + describe('Page.reload', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return ((globalThis as any)._foo = 10); + }); + await page.reload(); + expect( + await page.evaluate(() => { + return (globalThis as any)._foo; + }) + ).toBe(undefined); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/network.spec.ts b/remote/test/puppeteer/test/src/network.spec.ts new file mode 100644 index 0000000000..c6f51a3412 --- /dev/null +++ b/remote/test/puppeteer/test/src/network.spec.ts @@ -0,0 +1,917 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import type {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import type {HTTPResponse} from 'puppeteer-core/internal/api/HTTPResponse.js'; + +import {getTestState, launch, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('network', function () { + setupTestBrowserHooks(); + + describe('Page.Events.Request', function () { + it('should fire for navigation requests', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + }); + it('should fire for iframes', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests).toHaveLength(2); + }); + it('should fire for fetches', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + return fetch('/empty.html'); + }); + expect(requests).toHaveLength(2); + }); + }); + describe('Request.frame', function () { + it('should work for main frame navigation request', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.frame()).toBe(page.mainFrame()); + }); + it('should work for subframe navigation request', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.frame()).toBe(page.frames()[1]); + }); + it('should work for fetch requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let requests: HTTPRequest[] = []; + page.on('request', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.evaluate(() => { + return fetch('/digits/1.png'); + }); + requests = requests.filter(request => { + return !request.url().includes('favicon'); + }); + expect(requests).toHaveLength(1); + expect(requests[0]!.frame()).toBe(page.mainFrame()); + }); + }); + + describe('Request.headers', function () { + it('should define Browser in user agent header', async () => { + const {page, server, isChrome} = await getTestState(); + const response = (await page.goto(server.EMPTY_PAGE))!; + const userAgent = response.request().headers()['user-agent']; + + if (isChrome) { + expect(userAgent).toContain('Chrome'); + } else { + expect(userAgent).toContain('Firefox'); + } + }); + }); + + describe('Response.headers', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('foo', 'bar'); + res.end(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.headers()['foo']).toBe('bar'); + }); + }); + + describe('Request.initiator', () => { + it('should return the initiator', async () => { + const {page, server} = await getTestState(); + + const initiators = new Map(); + page.on('request', request => { + return initiators.set( + request.url().split('/').pop(), + request.initiator() + ); + }); + await page.goto(server.PREFIX + '/initiator.html'); + + expect(initiators.get('initiator.html').type).toBe('other'); + expect(initiators.get('initiator.js').type).toBe('parser'); + expect(initiators.get('initiator.js').url).toBe( + server.PREFIX + '/initiator.html' + ); + expect(initiators.get('frame.html').type).toBe('parser'); + expect(initiators.get('frame.html').url).toBe( + server.PREFIX + '/initiator.html' + ); + expect(initiators.get('script.js').type).toBe('parser'); + expect(initiators.get('script.js').url).toBe( + server.PREFIX + '/frames/frame.html' + ); + expect(initiators.get('style.css').type).toBe('parser'); + expect(initiators.get('style.css').url).toBe( + server.PREFIX + '/frames/frame.html' + ); + expect(initiators.get('initiator.js').type).toBe('parser'); + expect(initiators.get('injectedfile.js').type).toBe('script'); + expect(initiators.get('injectedfile.js').stack.callFrames[0]!.url).toBe( + server.PREFIX + '/initiator.js' + ); + expect(initiators.get('injectedstyle.css').type).toBe('script'); + expect(initiators.get('injectedstyle.css').stack.callFrames[0]!.url).toBe( + server.PREFIX + '/initiator.js' + ); + expect(initiators.get('initiator.js').url).toBe( + server.PREFIX + '/initiator.html' + ); + }); + }); + + describe('Response.fromCache', function () { + it('should return |false| for non-cached content', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.fromCache()).toBe(false); + }); + + it('should work', async () => { + const {page, server} = await getTestState(); + + const responses = new Map(); + page.on('response', r => { + return ( + !isFavicon(r.request()) && responses.set(r.url().split('/').pop(), r) + ); + }); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describe('Response.fromServiceWorker', function () { + it('should return |false| for non-service-worker content', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.fromServiceWorker()).toBe(false); + }); + + it('Response.fromServiceWorker', async () => { + const {page, server} = await getTestState(); + + const responses = new Map(); + page.on('response', r => { + return !isFavicon(r) && responses.set(r.url().split('/').pop(), r); + }); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', { + waitUntil: 'networkidle2', + }); + await page.evaluate(async () => { + return (globalThis as any).activationPromise; + }); + await page.reload(); + + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + }); + }); + + describe('Request.postData', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (_req, res) => { + return res.end(); + }); + + const [request] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'request', r => { + return !isFavicon(r); + }), + page.evaluate(() => { + return fetch('./post', { + method: 'POST', + body: JSON.stringify({foo: 'bar'}), + }); + }), + ]); + + expect(request).toBeTruthy(); + expect(request.postData()).toBe('{"foo":"bar"}'); + }); + + it('should be |undefined| when there is no post data', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.request().postData()).toBe(undefined); + }); + + it('should work with blobs', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRoute('/post', (_req, res) => { + return res.end(); + }); + + const [request] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'request', r => { + return !isFavicon(r); + }), + page.evaluate(() => { + return fetch('./post', { + method: 'POST', + body: new Blob([JSON.stringify({foo: 'bar'})], { + type: 'application/json', + }), + }); + }), + ]); + + expect(request).toBeTruthy(); + expect(request.postData()).toBe(undefined); + expect(request.hasPostData()).toBe(true); + expect(await request.fetchPostData()).toBe('{"foo":"bar"}'); + }); + }); + + describe('Response.text', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.PREFIX + '/simple.json'))!; + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should return uncompressed text', async () => { + const {page, server} = await getTestState(); + + server.enableGzip('/simple.json'); + const response = (await page.goto(server.PREFIX + '/simple.json'))!; + expect(response.headers()['content-encoding']).toBe('gzip'); + const responseText = (await response.text()).trimEnd(); + expect(responseText).toBe('{"foo": "bar"}'); + }); + it('should throw when requesting body of redirected response', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/foo.html', '/empty.html'); + const response = (await page.goto(server.PREFIX + '/foo.html'))!; + const redirectChain = response.request().redirectChain(); + expect(redirectChain).toHaveLength(1); + const redirected = redirectChain[0]!.response()!; + expect(redirected.status()).toBe(302); + let error!: Error; + await redirected.text().catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'Response body is unavailable for redirect responses' + ); + }); + it('should wait until response completes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + // Setup server to trap request. + let serverResponse!: ServerResponse; + server.setRoute('/get', (_req, res) => { + serverResponse = res; + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + res.setHeader('Content-Type', 'text/plain; charset=utf-8'); + res.write('hello '); + }); + // Setup page to trap response. + let requestFinished = false; + page.on('requestfinished', r => { + return (requestFinished = requestFinished || r.url().includes('/get')); + }); + // send request and wait for server response + const [pageResponse] = await Promise.all([ + page.waitForResponse(r => { + return !isFavicon(r.request()); + }), + page.evaluate(() => { + return fetch('./get', {method: 'GET'}); + }), + server.waitForRequest('/get'), + ]); + + expect(serverResponse).toBeTruthy(); + expect(pageResponse).toBeTruthy(); + expect(pageResponse.status()).toBe(200); + expect(requestFinished).toBe(false); + + const responseText = pageResponse.text(); + // Write part of the response and wait for it to be flushed. + await new Promise(x => { + return serverResponse.write('wor', x); + }); + // Finish response. + await new Promise<void>(x => { + serverResponse.end('ld!', () => { + return x(); + }); + }); + expect(await responseText).toBe('hello world!'); + }); + }); + + describe('Response.json', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.PREFIX + '/simple.json'))!; + expect(await response.json()).toEqual({foo: 'bar'}); + }); + }); + + describe('Response.buffer', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + const response = (await page.goto(server.PREFIX + '/pptr.png'))!; + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should work with compression', async () => { + const {page, server} = await getTestState(); + + server.enableGzip('/pptr.png'); + const response = (await page.goto(server.PREFIX + '/pptr.png'))!; + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + const responseBuffer = await response.buffer(); + expect(responseBuffer.equals(imageBuffer)).toBe(true); + }); + it('should throw if the response does not have a body', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/empty.html'); + + server.setRoute('/test.html', (_req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Headers', 'x-ping'); + res.end('Hello World'); + }); + const url = server.CROSS_PROCESS_PREFIX + '/test.html'; + const responsePromise = waitEvent<HTTPResponse>( + page, + 'response', + response => { + // Get the preflight response. + return ( + response.request().method() === 'OPTIONS' && response.url() === url + ); + } + ); + + // Trigger a request with a preflight. + await page.evaluate(async src => { + const response = await fetch(src, { + method: 'POST', + headers: {'x-ping': 'pong'}, + }); + return response; + }, url); + + const response = await responsePromise; + await expect(response.buffer()).rejects.toThrowError( + 'Could not load body for this request. This might happen if the request is a preflight request.' + ); + }); + }); + + describe('Response.statusText', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/cool', (_req, res) => { + res.writeHead(200, 'cool!'); + res.end(); + }); + const response = (await page.goto(server.PREFIX + '/cool'))!; + expect(response.statusText()).toBe('cool!'); + }); + + it('handles missing status text', async () => { + const {page, server} = await getTestState(); + + server.setRoute('/nostatus', (_req, res) => { + res.writeHead(200, ''); + res.end(); + }); + const response = (await page.goto(server.PREFIX + '/nostatus'))!; + expect(response.statusText()).toBe(''); + }); + }); + + describe('Response.timing', function () { + it('returns timing information', async () => { + const {page, server} = await getTestState(); + const responses: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + await page.goto(server.EMPTY_PAGE); + expect(responses).toHaveLength(1); + expect(responses[0]!.timing()!.receiveHeadersEnd).toBeGreaterThan(0); + }); + }); + + describe('Network Events', function () { + it('Page.Events.Request', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('request', request => { + return requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[0]!.method()).toBe('GET'); + expect(requests[0]!.response()).toBeTruthy(); + expect(requests[0]!.frame() === page.mainFrame()).toBe(true); + expect(requests[0]!.frame()!.url()).toBe(server.EMPTY_PAGE); + }); + it('Page.Events.RequestServedFromCache', async () => { + const {page, server} = await getTestState(); + + const cached: string[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r.url().split('/').pop()!); + }); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + expect(cached).toEqual([]); + + await page.reload(); + expect(cached).toEqual(['one-style.css']); + }); + it('Page.Events.Response', async () => { + const {page, server} = await getTestState(); + + const responses: HTTPResponse[] = []; + page.on('response', response => { + return responses.push(response); + }); + await page.goto(server.EMPTY_PAGE); + expect(responses).toHaveLength(1); + expect(responses[0]!.url()).toBe(server.EMPTY_PAGE); + expect(responses[0]!.status()).toBe(200); + expect(responses[0]!.ok()).toBe(true); + expect(responses[0]!.request()).toBeTruthy(); + const remoteAddress = responses[0]!.remoteAddress(); + // Either IPv6 or IPv4, depending on environment. + expect( + remoteAddress.ip!.includes('::1') || remoteAddress.ip === '127.0.0.1' + ).toBe(true); + expect(remoteAddress.port).toBe(server.PORT); + }); + + it('Page.Events.RequestFailed', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('css')) { + void request.abort(); + } else { + void request.continue(); + } + }); + const failedRequests: HTTPRequest[] = []; + page.on('requestfailed', request => { + return failedRequests.push(request); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(failedRequests).toHaveLength(1); + expect(failedRequests[0]!.url()).toContain('one-style.css'); + expect(failedRequests[0]!.response()).toBe(null); + expect(failedRequests[0]!.resourceType()).toBe('stylesheet'); + if (isChrome) { + expect(failedRequests[0]!.failure()!.errorText).toBe('net::ERR_FAILED'); + } else { + expect(failedRequests[0]!.failure()!.errorText).toBe( + 'NS_ERROR_FAILURE' + ); + } + expect(failedRequests[0]!.frame()).toBeTruthy(); + }); + it('Page.Events.RequestFinished', async () => { + const {page, server} = await getTestState(); + + const requests: HTTPRequest[] = []; + page.on('requestfinished', request => { + return !isFavicon(request) && requests.push(request); + }); + await page.goto(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + const request = requests[0]!; + expect(request.url()).toBe(server.EMPTY_PAGE); + expect(request.response()).toBeTruthy(); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame()!.url()).toBe(server.EMPTY_PAGE); + }); + it('should fire events in proper order', async () => { + const {page, server} = await getTestState(); + + const events: string[] = []; + page.on('request', () => { + return events.push('request'); + }); + page.on('response', () => { + return events.push('response'); + }); + page.on('requestfinished', () => { + return events.push('requestfinished'); + }); + await page.goto(server.EMPTY_PAGE); + // Events can sneak in after the page has navigate + expect(events.slice(0, 3)).toEqual([ + 'request', + 'response', + 'requestfinished', + ]); + }); + it('should support redirects', async () => { + const {page, server} = await getTestState(); + + const events: string[] = []; + page.on('request', request => { + return events.push(`${request.method()} ${request.url()}`); + }); + page.on('response', response => { + return events.push(`${response.status()} ${response.url()}`); + }); + page.on('requestfinished', request => { + return events.push(`DONE ${request.url()}`); + }); + page.on('requestfailed', request => { + return events.push(`FAIL ${request.url()}`); + }); + server.setRedirect('/foo.html', '/empty.html'); + const FOO_URL = server.PREFIX + '/foo.html'; + const response = (await page.goto(FOO_URL))!; + expect(events).toEqual([ + `GET ${FOO_URL}`, + `302 ${FOO_URL}`, + `DONE ${FOO_URL}`, + `GET ${server.EMPTY_PAGE}`, + `200 ${server.EMPTY_PAGE}`, + `DONE ${server.EMPTY_PAGE}`, + ]); + + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain).toHaveLength(1); + expect(redirectChain[0]!.url()).toContain('/foo.html'); + expect(redirectChain[0]!.response()!.remoteAddress().port).toBe( + server.PORT + ); + }); + }); + + describe('Request.isNavigationRequest', () => { + it('should work', async () => { + const {page, server} = await getTestState(); + + const requests = new Map(); + page.on('request', request => { + return requests.set(request.url().split('/').pop(), request); + }); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it('should work with request interception', async () => { + const {page, server} = await getTestState(); + + const requests = new Map(); + page.on('request', request => { + requests.set(request.url().split('/').pop(), request); + void request.continue(); + }); + await page.setRequestInterception(true); + server.setRedirect('/rrredirect', '/frames/one-frame.html'); + await page.goto(server.PREFIX + '/rrredirect'); + expect(requests.get('rrredirect').isNavigationRequest()).toBe(true); + expect(requests.get('one-frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('frame.html').isNavigationRequest()).toBe(true); + expect(requests.get('script.js').isNavigationRequest()).toBe(false); + expect(requests.get('style.css').isNavigationRequest()).toBe(false); + }); + it('should work when navigating to image', async () => { + const {page, server} = await getTestState(); + + const [request] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'request'), + page.goto(server.PREFIX + '/pptr.png'), + ]); + expect(request.isNavigationRequest()).toBe(true); + }); + }); + + describe('Page.setExtraHTTPHeaders', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should throw for non-string header values', async () => { + const {page} = await getTestState(); + + let error!: Error; + try { + // @ts-expect-error purposeful bad input + await page.setExtraHTTPHeaders({foo: 1}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Expected value of header "foo" to be String, but "number" is found.' + ); + }); + }); + + describe('Page.authenticate', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + server.setAuth('/empty.html', 'user', 'pass'); + let response; + try { + response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(401); + } catch (error) { + // In headful, an error is thrown instead of 401. + if ( + !(error as Error).message.startsWith( + 'net::ERR_INVALID_AUTH_CREDENTIALS' + ) + ) { + throw error; + } + } + await page.authenticate({ + username: 'user', + password: 'pass', + }); + response = (await page.reload())!; + expect(response.status()).toBe(200); + }); + it('should fail if wrong credentials', async () => { + const {page, server} = await getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user2', 'pass2'); + await page.authenticate({ + username: 'foo', + password: 'bar', + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(401); + }); + it('should allow disable authentication', async () => { + const {page, server} = await getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/empty.html', 'user3', 'pass3'); + await page.authenticate({ + username: 'user3', + password: 'pass3', + }); + let response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(200); + await page.authenticate({ + username: '', + password: '', + }); + // Navigate to a different origin to bust Chrome's credential caching. + try { + response = (await page.goto( + server.CROSS_PROCESS_PREFIX + '/empty.html' + ))!; + expect(response.status()).toBe(401); + } catch (error) { + // In headful, an error is thrown instead of 401. + if ( + !(error as Error).message.startsWith( + 'net::ERR_INVALID_AUTH_CREDENTIALS' + ) + ) { + throw error; + } + } + }); + it('should not disable caching', async () => { + const {page, server} = await getTestState(); + + // Use unique user/password since Chrome caches credentials per origin. + server.setAuth('/cached/one-style.css', 'user4', 'pass4'); + server.setAuth('/cached/one-style.html', 'user4', 'pass4'); + await page.authenticate({ + username: 'user4', + password: 'pass4', + }); + + const responses = new Map(); + page.on('response', r => { + return responses.set(r.url().split('/').pop(), r); + }); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + await page.reload(); + + expect(responses.get('one-style.css').status()).toBe(200); + expect(responses.get('one-style.css').fromCache()).toBe(true); + expect(responses.get('one-style.html').status()).toBe(304); + expect(responses.get('one-style.html').fromCache()).toBe(false); + }); + }); + + describe('raw network headers', () => { + it('Same-origin set-cookie navigation', async () => { + const {page, server} = await getTestState(); + + const setCookieString = 'foo=bar'; + server.setRoute('/empty.html', (_req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Same-origin set-cookie subresource', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + + const setCookieString = 'foo=bar'; + server.setRoute('/foo', (_req, res) => { + res.setHeader('set-cookie', setCookieString); + res.end('hello world'); + }); + + const [response] = await Promise.all([ + waitEvent<HTTPResponse>(page, 'response', res => { + return !isFavicon(res); + }), + page.evaluate(() => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', '/foo'); + xhr.send(); + }), + ]); + expect(response.headers()['set-cookie']).toBe(setCookieString); + }); + + it('Cross-origin set-cookie', async () => { + const {page, httpsServer, close} = await launch({ + ignoreHTTPSErrors: true, + }); + try { + await page.goto(httpsServer.PREFIX + '/empty.html'); + + const setCookieString = 'hello=world'; + httpsServer.setRoute('/setcookie.html', (_req, res) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('set-cookie', setCookieString); + res.end(); + }); + await page.goto(httpsServer.PREFIX + '/setcookie.html'); + const url = httpsServer.CROSS_PROCESS_PREFIX + '/setcookie.html'; + const [response] = await Promise.all([ + waitEvent<HTTPResponse>(page, 'response', response => { + return response.url() === url; + }), + page.evaluate(src => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', src); + xhr.send(); + }, url), + ]); + expect(response.headers()['set-cookie']).toBe(setCookieString); + } finally { + await close(); + } + }); + }); + + describe('Page.setBypassServiceWorker', () => { + it('bypass for network', async () => { + const {page, server} = await getTestState(); + + const responses = new Map(); + page.on('response', r => { + return !isFavicon(r) && responses.set(r.url().split('/').pop(), r); + }); + + // Load and re-load to make sure serviceworker is installed and running. + await page.goto(server.PREFIX + '/serviceworkers/fetch/sw.html', { + waitUntil: 'networkidle2', + }); + await page.evaluate(async () => { + return (globalThis as any).activationPromise; + }); + await page.reload({ + waitUntil: 'networkidle2', + }); + + expect(page.isServiceWorkerBypassed()).toBe(false); + expect(responses.size).toBe(2); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(true); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(true); + + await page.setBypassServiceWorker(true); + await page.reload({ + waitUntil: 'networkidle2', + }); + + expect(page.isServiceWorkerBypassed()).toBe(true); + expect(responses.get('sw.html').status()).toBe(200); + expect(responses.get('sw.html').fromServiceWorker()).toBe(false); + expect(responses.get('style.css').status()).toBe(200); + expect(responses.get('style.css').fromServiceWorker()).toBe(false); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/oopif.spec.ts b/remote/test/puppeteer/test/src/oopif.spec.ts new file mode 100644 index 0000000000..c024b76aba --- /dev/null +++ b/remote/test/puppeteer/test/src/oopif.spec.ts @@ -0,0 +1,527 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {BrowserContext} from 'puppeteer-core/internal/api/BrowserContext.js'; +import type {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js'; +import {CDPSessionEvent} from 'puppeteer-core/internal/api/CDPSession.js'; +import type {CdpTarget} from 'puppeteer-core/internal/cdp/Target.js'; + +import {getTestState, launch} from './mocha-utils.js'; +import {attachFrame, detachFrame, navigateFrame} from './utils.js'; + +describe('OOPIF', function () { + /* We use a special browser for this test as we need the --site-per-process flag */ + let state: Awaited<ReturnType<typeof launch>>; + + before(async () => { + const {defaultBrowserOptions} = await getTestState({skipLaunch: true}); + + state = await launch( + Object.assign({}, defaultBrowserOptions, { + args: (defaultBrowserOptions.args || []).concat([ + '--site-per-process', + '--remote-debugging-port=21222', + '--host-rules=MAP * 127.0.0.1', + ]), + }), + {after: 'all'} + ); + }); + + beforeEach(async () => { + state.context = await state.browser.createIncognitoBrowserContext(); + state.page = await state.context.newPage(); + }); + + afterEach(async () => { + await state.context.close(); + }); + + after(async () => { + await state.close(); + }); + + it('should treat OOP iframes and normal iframes the same', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/empty.html'); + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await framePromise; + expect(page.mainFrame().childFrames()).toHaveLength(2); + }); + it('should track navigations within OOP iframes', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/assets/frame.html' + ); + expect(frame.url()).toContain('/assets/frame.html'); + }); + it('should support OOP iframes becoming normal iframes again', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.isOOPFrame()).toBe(false); + expect(page.frames()).toHaveLength(2); + }); + it('should support frames within OOP frames', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const frame1Promise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + const frame2Promise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 2; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/frames/one-frame.html' + ); + + const [frame1, frame2] = await Promise.all([frame1Promise, frame2Promise]); + + expect( + await frame1.evaluate(() => { + return document.location.href; + }) + ).toMatch(/one-frame\.html$/); + expect( + await frame2.evaluate(() => { + return document.location.href; + }) + ).toMatch(/frames\/frame\.html$/); + }); + it('should support OOP iframes getting detached', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(frame.isOOPFrame()).toBe(true); + await detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should support wait for navigation for transitions from local to OOPIF', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + + const frame = await framePromise; + expect(frame.isOOPFrame()).toBe(false); + const nav = frame.waitForNavigation(); + await navigateFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + await nav; + expect(frame.isOOPFrame()).toBe(true); + await detachFrame(page, 'frame1'); + expect(page.frames()).toHaveLength(1); + }); + + it('should keep track of a frames OOP state', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + expect(frame.url()).toContain('/empty.html'); + await navigateFrame(page, 'frame1', server.EMPTY_PAGE); + expect(frame.url()).toBe(server.EMPTY_PAGE); + }); + + it('should support evaluating in oop iframes', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + _test = 'Test 123!'; + }); + const result = await frame.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + return window._test; + }); + expect(result).toBe('Test 123!'); + }); + it('should provide access to elements', async () => { + const {server, isHeadless, headless, page} = state; + + if (!isHeadless || headless === 'new') { + // TODO: this test is partially blocked on crbug.com/1334119. Enable test once + // the upstream is fixed. + // TLDR: when we dispatch events to the frame the compositor might + // not be up-to-date yet resulting in a misclick (the iframe element + // becomes the event target instead of the content inside the iframe). + // The solution is to use InsertVisualCallback on the backend but that causes + // another issue that events cannot be dispatched to inactive tabs as the + // visual callback is never invoked. + // The old headless mode does not have this issue since it operates with + // special scheduling settings that keep even inactive tabs updating. + return; + } + + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame = await framePromise; + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + button.onclick = () => { + button.id = 'clicked'; + }; + document.body.appendChild(button); + }); + await page.evaluate(() => { + document.body.style.border = '150px solid black'; + document.body.style.margin = '250px'; + document.body.style.padding = '50px'; + }); + await frame.waitForSelector('#test-button', {visible: true}); + await frame.click('#test-button'); + await frame.waitForSelector('#clicked'); + }); + it('should report oopif frames', async () => { + const {server, page, context} = state; + + const frame = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(1); + expect(page.frames()).toHaveLength(2); + }); + + it('should wait for inner OOPIFs', async () => { + const {server, page, context} = state; + await page.goto(`http://mainframe:${server.PORT}/main-frame.html`); + const frame2 = await page.waitForFrame(frame => { + return frame.url().endsWith('inner-frame2.html'); + }); + expect(oopifs(context)).toHaveLength(2); + expect( + page.frames().filter(frame => { + return frame.isOOPFrame(); + }) + ).toHaveLength(2); + expect( + await frame2.evaluate(() => { + return document.querySelectorAll('button').length; + }) + ).toStrictEqual(1); + }); + + it('should load oopif iframes with subresources and request interception', async () => { + const {server, page, context} = state; + + const framePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + page.on('request', request => { + void request.continue(); + }); + await page.setRequestInterception(true); + const requestPromise = page.waitForRequest(request => { + return request.url().includes('requestFromOOPIF'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + const frame = await framePromise; + const request = await requestPromise; + expect(oopifs(context)).toHaveLength(1); + expect(request.frame()).toBe(frame); + }); + + it('should support frames within OOP iframes', async () => { + const {server, page} = state; + + const oopIframePromise = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + const oopIframe = await oopIframePromise; + await attachFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + const frame1 = oopIframe.childFrames()[0]!; + expect(frame1.url()).toMatch(/empty.html$/); + await navigateFrame( + oopIframe, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/oopif.html' + ); + expect(frame1.url()).toMatch(/oopif.html$/); + await frame1.goto( + server.CROSS_PROCESS_PREFIX + '/oopif.html#navigate-within-document', + {waitUntil: 'load'} + ); + expect(frame1.url()).toMatch(/oopif.html#navigate-within-document$/); + await detachFrame(oopIframe, 'frame1'); + expect(oopIframe.childFrames()).toHaveLength(0); + }); + + it('clickablePoint, boundingBox, boxModel should work for elements inside OOPIFs', async () => { + const {server, page} = state; + await page.goto(server.EMPTY_PAGE); + const framePromise = page.waitForFrame(frame => { + return page.frames().indexOf(frame) === 1; + }); + await attachFrame( + page, + 'frame1', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + const frame = await framePromise; + await page.evaluate(() => { + document.body.style.border = '50px solid black'; + document.body.style.margin = '50px'; + document.body.style.padding = '50px'; + }); + await frame.evaluate(() => { + const button = document.createElement('button'); + button.id = 'test-button'; + button.innerText = 'click'; + document.body.appendChild(button); + }); + using button = (await frame.waitForSelector('#test-button', { + visible: true, + }))!; + const result = await button.clickablePoint(); + expect(result.x).toBeGreaterThan(150); // padding + margin + border left + expect(result.y).toBeGreaterThan(150); // padding + margin + border top + const resultBoxModel = (await button.boxModel())!; + for (const quad of [ + resultBoxModel.content, + resultBoxModel.border, + resultBoxModel.margin, + resultBoxModel.padding, + ]) { + for (const part of quad) { + expect(part.x).toBeGreaterThan(150); // padding + margin + border left + expect(part.y).toBeGreaterThan(150); // padding + margin + border top + } + } + const resultBoundingBox = (await button.boundingBox())!; + expect(resultBoundingBox.x).toBeGreaterThan(150); // padding + margin + border left + expect(resultBoundingBox.y).toBeGreaterThan(150); // padding + margin + border top + }); + + it('should detect existing OOPIFs when Puppeteer connects to an existing page', async () => { + const {server, puppeteer, page, context} = state; + + const frame = page.waitForFrame(frame => { + return frame.url().endsWith('/oopif.html'); + }); + await page.goto(server.PREFIX + '/dynamic-oopif.html'); + await frame; + expect(oopifs(context)).toHaveLength(1); + expect(page.frames()).toHaveLength(2); + + const browserURL = 'http://127.0.0.1:21222'; + const browser1 = await puppeteer.connect({browserURL}); + const target = await browser1.waitForTarget(target => { + return target.url().endsWith('dynamic-oopif.html'); + }); + await target.page(); + await browser1.disconnect(); + }); + + it('should support lazy OOP frames', async () => { + const {server, page} = state; + + await page.goto(server.PREFIX + '/lazy-oopif-frame.html'); + await page.setViewport({width: 1000, height: 1000}); + + expect( + page.frames().map(frame => { + return frame._hasStartedLoading; + }) + ).toEqual([true, true, false]); + }); + + describe('waitForFrame', () => { + it('should resolve immediately if the frame already exists', async () => { + const {server, page} = state; + + await page.goto(server.EMPTY_PAGE); + await attachFrame( + page, + 'frame2', + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + + await page.waitForFrame(frame => { + return frame.url().endsWith('/empty.html'); + }); + }); + }); + + it('should report google.com frame', async () => { + const {server, page} = state; + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', r => { + return r.respond({body: 'YO, GOOGLE.COM'}); + }); + await page.evaluate(() => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', 'https://google.com/'); + document.body.appendChild(frame); + return new Promise(x => { + return (frame.onload = x); + }); + }); + await page.waitForSelector('iframe[src="https://google.com/"]'); + const urls = page + .frames() + .map(frame => { + return frame.url(); + }) + .sort(); + expect(urls).toEqual([server.EMPTY_PAGE, 'https://google.com/']); + }); + + it('should expose events within OOPIFs', async () => { + const {server, page} = state; + + // Setup our session listeners to observe OOPIF activity. + const session = await page.target().createCDPSession(); + const networkEvents: string[] = []; + const otherSessions: CDPSession[] = []; + await session.send('Target.setAutoAttach', { + autoAttach: true, + flatten: true, + waitForDebuggerOnStart: true, + }); + session.on(CDPSessionEvent.SessionAttached, async session => { + otherSessions.push(session); + + session.on('Network.requestWillBeSent', params => { + return networkEvents.push(params.request.url); + }); + await session.send('Network.enable'); + await session.send('Runtime.runIfWaitingForDebugger'); + }); + + // Navigate to the empty page and add an OOPIF iframe with at least one request. + await page.goto(server.EMPTY_PAGE); + await page.evaluate( + (frameUrl: string) => { + const frame = document.createElement('iframe'); + frame.setAttribute('src', frameUrl); + document.body.appendChild(frame); + return new Promise((x, y) => { + frame.onload = x; + frame.onerror = y; + }); + }, + server.PREFIX.replace('localhost', 'oopifdomain') + '/one-style.html' + ); + await page.waitForSelector('iframe'); + + // Ensure we found the iframe session. + expect(otherSessions).toHaveLength(1); + + // Resume the iframe and trigger another request. + const iframeSession = otherSessions[0]!; + await iframeSession.send('Runtime.evaluate', { + expression: `fetch('/fetch')`, + awaitPromise: true, + }); + + expect(networkEvents).toContain(`http://oopifdomain:${server.PORT}/fetch`); + }); +}); + +function oopifs(context: BrowserContext) { + return context.targets().filter(target => { + return (target as CdpTarget)._getTargetInfo().type === 'iframe'; + }); +} diff --git a/remote/test/puppeteer/test/src/page.spec.ts b/remote/test/puppeteer/test/src/page.spec.ts new file mode 100644 index 0000000000..79fc69ebbc --- /dev/null +++ b/remote/test/puppeteer/test/src/page.spec.ts @@ -0,0 +1,2287 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert'; +import fs from 'fs'; +import type {ServerResponse} from 'http'; +import path from 'path'; + +import expect from 'expect'; +import {KnownDevices, TimeoutError} from 'puppeteer'; +import {CDPSession} from 'puppeteer-core/internal/api/CDPSession.js'; +import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import type {Metrics, Page} from 'puppeteer-core/internal/api/Page.js'; +import type {CdpPage} from 'puppeteer-core/internal/cdp/Page.js'; +import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; +import sinon from 'sinon'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {attachFrame, detachFrame, isFavicon, waitEvent} from './utils.js'; + +describe('Page', function () { + setupTestBrowserHooks(); + + describe('Page.close', function () { + it('should reject all promises when page is closed', async () => { + const {context} = await getTestState(); + + const newPage = await context.newPage(); + let error!: Error; + await Promise.all([ + newPage + .evaluate(() => { + return new Promise(() => {}); + }) + .catch(error_ => { + return (error = error_); + }), + newPage.close(), + ]); + expect(error.message).toContain('Protocol error'); + }); + it('should not be visible in browser.pages', async () => { + const {browser} = await getTestState(); + + const newPage = await browser.newPage(); + expect(await browser.pages()).toContain(newPage); + await newPage.close(); + expect(await browser.pages()).not.toContain(newPage); + }); + it('should run beforeunload if asked for', async () => { + const {context, server, isChrome} = await getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + const pageClosingPromise = newPage.close({runBeforeUnload: true}); + const dialog = await waitEvent(newPage, 'dialog'); + expect(dialog.type()).toBe('beforeunload'); + expect(dialog.defaultValue()).toBe(''); + if (isChrome) { + expect(dialog.message()).toBe(''); + } else { + expect(dialog.message()).toBeTruthy(); + } + await dialog.accept(); + await pageClosingPromise; + }); + it('should *not* run beforeunload by default', async () => { + const {context, server} = await getTestState(); + + const newPage = await context.newPage(); + await newPage.goto(server.PREFIX + '/beforeunload.html'); + // We have to interact with a page so that 'beforeunload' handlers + // fire. + await newPage.click('body'); + await newPage.close(); + }); + it('should set the page close state', async () => { + const {context} = await getTestState(); + + const newPage = await context.newPage(); + expect(newPage.isClosed()).toBe(false); + await newPage.close(); + expect(newPage.isClosed()).toBe(true); + }); + it('should terminate network waiters', async () => { + const {context, server} = await getTestState(); + + const newPage = await context.newPage(); + const results = await Promise.all([ + newPage.waitForRequest(server.EMPTY_PAGE).catch(error => { + return error; + }), + newPage.waitForResponse(server.EMPTY_PAGE).catch(error => { + return error; + }), + newPage.close(), + ]); + for (let i = 0; i < 2; i++) { + const message = results[i].message; + expect(message).atLeastOneToContain(['Target closed', 'Page closed!']); + expect(message).not.toContain('Timeout'); + } + }); + }); + + describe('Page.Events.Load', function () { + it('should fire when expected', async () => { + const {page} = await getTestState(); + + await Promise.all([waitEvent(page, 'load'), page.goto('about:blank')]); + }); + }); + + describe('removing and adding event handlers', () => { + it('should correctly fire event handlers as they are added and then removed', async () => { + const {page, server} = await getTestState(); + + const handler = sinon.spy(); + const onResponse = (response: {url: () => string}) => { + // Ignore default favicon requests. + if (!isFavicon(response)) { + handler(); + } + }; + page.on('response', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(1); + page.off('response', onResponse); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(1); + page.on('response', onResponse); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(2); + }); + + it('should correctly added and removed request events', async () => { + const {page, server} = await getTestState(); + + const handler = sinon.spy(); + const onResponse = (response: {url: () => string}) => { + // Ignore default favicon requests. + if (!isFavicon(response)) { + handler(); + } + }; + + page.on('request', onResponse); + page.on('request', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(2); + page.off('request', onResponse); + await page.goto(server.EMPTY_PAGE); + // Still one because we removed the handler. + expect(handler.callCount).toBe(3); + page.off('request', onResponse); + await page.goto(server.EMPTY_PAGE); + expect(handler.callCount).toBe(3); + page.on('request', onResponse); + await page.goto(server.EMPTY_PAGE); + // Two now because we added the handler back. + expect(handler.callCount).toBe(4); + }); + }); + + describe('Page.Events.error', function () { + it('should throw when page crashes', async () => { + const {page, isChrome} = await getTestState(); + + let navigate: Promise<unknown>; + if (isChrome) { + navigate = page.goto('chrome://crash').catch(() => {}); + } else { + navigate = page.goto('about:crashcontent').catch(() => {}); + } + const [error] = await Promise.all([ + waitEvent<Error>(page, 'error'), + navigate, + ]); + expect(error.message).toBe('Page crashed!'); + }); + }); + + describe('Page.Events.Popup', function () { + it('should work', async () => { + const {page} = await getTestState(); + + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.evaluate(() => { + return window.open('about:blank'); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(true); + }); + it('should work with noopener', async () => { + const {page} = await getTestState(); + + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.evaluate(() => { + return window.open('about:blank', undefined, 'noopener'); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + it('should work with clicking target=_blank and without rel=opener', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent('<a target=_blank href="/one-style.html">yo</a>'); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + it('should work with clicking target=_blank and with rel=opener', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + '<a target=_blank rel=opener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(true); + }); + it('should work with fake-clicking target=_blank and rel=noopener', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.$eval('a', a => { + return (a as HTMLAnchorElement).click(); + }), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + it('should work with clicking target=_blank and rel=noopener', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setContent( + '<a target=_blank rel=noopener href="/one-style.html">yo</a>' + ); + const [popup] = await Promise.all([ + waitEvent<Page>(page, 'popup'), + page.click('a'), + ]); + expect( + await page.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + expect( + await popup.evaluate(() => { + return !!window.opener; + }) + ).toBe(false); + }); + }); + + describe('Page.setGeolocation', function () { + it('should work', async () => { + const {page, server, context} = await getTestState(); + + await context.overridePermissions(server.PREFIX, ['geolocation']); + await page.goto(server.EMPTY_PAGE); + await page.setGeolocation({longitude: 10, latitude: 10}); + const geolocation = await page.evaluate(() => { + return new Promise(resolve => { + return navigator.geolocation.getCurrentPosition(position => { + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }); + }); + }); + }); + expect(geolocation).toEqual({ + latitude: 10, + longitude: 10, + }); + }); + it('should throw when invalid longitude', async () => { + const {page} = await getTestState(); + + let error!: Error; + try { + await page.setGeolocation({longitude: 200, latitude: 10}); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain('Invalid longitude "200"'); + }); + }); + + describe('Page.setOfflineMode', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setOfflineMode(true); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + await page.setOfflineMode(false); + const response = (await page.reload())!; + expect(response.status()).toBe(200); + }); + it('should emulate navigator.onLine', async () => { + const {page} = await getTestState(); + + expect( + await page.evaluate(() => { + return window.navigator.onLine; + }) + ).toBe(true); + await page.setOfflineMode(true); + expect( + await page.evaluate(() => { + return window.navigator.onLine; + }) + ).toBe(false); + await page.setOfflineMode(false); + expect( + await page.evaluate(() => { + return window.navigator.onLine; + }) + ).toBe(true); + }); + }); + + describe('Page.Events.Console', function () { + it('should work', async () => { + const {page} = await getTestState(); + + const [message] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.evaluate(() => { + return console.log('hello', 5, {foo: 'bar'}); + }), + ]); + expect(message.text()).toEqual('hello 5 JSHandle@object'); + expect(message.type()).toEqual('log'); + expect(message.args()).toHaveLength(3); + expect(message.location()).toEqual({ + url: expect.any(String), + lineNumber: expect.any(Number), + columnNumber: expect.any(Number), + }); + + expect(await message.args()[0]!.jsonValue()).toEqual('hello'); + expect(await message.args()[1]!.jsonValue()).toEqual(5); + expect(await message.args()[2]!.jsonValue()).toEqual({foo: 'bar'}); + }); + it('should work on script call right after navigation', async () => { + const {page} = await getTestState(); + + const [message] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.goto( + // Firefox prints warn if <!DOCTYPE html> is not present + `data:text/html,<!DOCTYPE html><script>console.log('SOME_LOG_MESSAGE');</script>` + ), + ]); + + expect(message.text()).toEqual('SOME_LOG_MESSAGE'); + }); + it('should work for different console API calls with logging functions', async () => { + const {page} = await getTestState(); + + const messages: ConsoleMessage[] = []; + page.on('console', msg => { + return messages.push(msg); + }); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + console.trace('calling console.trace'); + console.dir('calling console.dir'); + console.warn('calling console.warn'); + console.error('calling console.error'); + console.log(Promise.resolve('should not wait until resolved!')); + }); + expect( + messages.map(msg => { + return msg.type(); + }) + ).toEqual(['trace', 'dir', 'warning', 'error', 'log']); + expect( + messages.map(msg => { + return msg.text(); + }) + ).toEqual([ + 'calling console.trace', + 'calling console.dir', + 'calling console.warn', + 'calling console.error', + 'JSHandle@promise', + ]); + }); + it('should work for different console API calls with timing functions', async () => { + const {page} = await getTestState(); + + const messages: any[] = []; + page.on('console', msg => { + return messages.push(msg); + }); + // All console events will be reported before `page.evaluate` is finished. + await page.evaluate(() => { + // A pair of time/timeEnd generates only one Console API call. + console.time('calling console.time'); + console.timeEnd('calling console.time'); + }); + expect( + messages.map(msg => { + return msg.type(); + }) + ).toEqual(['timeEnd']); + expect(messages[0]!.text()).toContain('calling console.time'); + }); + it('should not fail for window object', async () => { + const {page} = await getTestState(); + + const [message] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.evaluate(() => { + return console.error(window); + }), + ]); + expect(message.text()).atLeastOneToContain([ + 'JSHandle@object', + 'JSHandle@window', + ]); + }); + it('should trigger correct Log', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.goto('about:blank'); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(async (url: string) => { + return await fetch(url).catch(() => {}); + }, server.EMPTY_PAGE), + ]); + expect(message.text()).toContain('Access-Control-Allow-Origin'); + if (isChrome) { + expect(message.type()).toEqual('error'); + } else { + expect(message.type()).toEqual('warn'); + } + }); + it('should have location when fetch fails', async () => { + const {page, server} = await getTestState(); + + // The point of this test is to make sure that we report console messages from + // Log domain: https://vanilla.aslushnikov.com/?Log.entryAdded + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.setContent(`<script>fetch('http://wat');</script>`), + ]); + expect(message.text()).toContain(`ERR_NAME_NOT_RESOLVED`); + expect(message.type()).toEqual('error'); + expect(message.location()).toEqual({ + url: 'http://wat/', + lineNumber: undefined, + }); + }); + it('should have location and stack trace for console API calls', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.goto(server.PREFIX + '/consolelog.html'), + ]); + expect(message.text()).toBe('yellow'); + expect(message.type()).toBe('log'); + expect(message.location()).toEqual({ + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }); + expect(message.stackTrace()).toEqual([ + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 8, + columnNumber: isChrome ? 16 : 8, // console.|log vs |console.log + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 11, + columnNumber: 8, + }, + { + url: server.PREFIX + '/consolelog.html', + lineNumber: 13, + columnNumber: 6, + }, + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3865 + it('should not throw when there are console messages in detached iframes', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(async () => { + // 1. Create a popup that Puppeteer is not connected to. + const win = window.open( + window.location.href, + 'Title', + 'toolbar=no,location=no,directories=no,status=no,menubar=no,scrollbars=yes,resizable=yes,width=780,height=200,top=0,left=0' + )!; + await new Promise(x => { + return (win.onload = x); + }); + // 2. In this popup, create an iframe that console.logs a message. + win.document.body.innerHTML = `<iframe src='/consolelog.html'></iframe>`; + const frame = win.document.querySelector('iframe')!; + await new Promise(x => { + return (frame.onload = x); + }); + // 3. After that, remove the iframe. + frame.remove(); + }); + const popupTarget = page + .browserContext() + .targets() + .find(target => { + return target !== page.target(); + })!; + // 4. Connect to the popup and make sure it doesn't throw. + await popupTarget.page(); + }); + }); + + describe('Page.Events.DOMContentLoaded', function () { + it('should fire when expected', async () => { + const {page} = await getTestState(); + + const navigate = page.goto('about:blank'); + await Promise.all([waitEvent(page, 'domcontentloaded'), navigate]); + }); + }); + + describe('Page.metrics', function () { + it('should get metrics from a page', async () => { + const {page} = await getTestState(); + + await page.goto('about:blank'); + const metrics = await page.metrics(); + checkMetrics(metrics); + }); + it('metrics event fired on console.timeStamp', async () => { + const {page} = await getTestState(); + + const metricsPromise = waitEvent<{metrics: Metrics; title: string}>( + page, + 'metrics' + ); + + await page.evaluate(() => { + return console.timeStamp('test42'); + }); + const metrics = await metricsPromise; + expect(metrics.title).toBe('test42'); + checkMetrics(metrics.metrics); + }); + function checkMetrics(metrics: Metrics) { + const metricsToCheck = new Set([ + 'Timestamp', + 'Documents', + 'Frames', + 'JSEventListeners', + 'Nodes', + 'LayoutCount', + 'RecalcStyleCount', + 'LayoutDuration', + 'RecalcStyleDuration', + 'ScriptDuration', + 'TaskDuration', + 'JSHeapUsedSize', + 'JSHeapTotalSize', + ]); + for (const name in metrics) { + expect(metricsToCheck.has(name)).toBeTruthy(); + expect(metrics[name as keyof Metrics]).toBeGreaterThanOrEqual(0); + metricsToCheck.delete(name); + } + expect(metricsToCheck.size).toBe(0); + } + }); + + describe('Page.waitForRequest', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with predicate', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(request => { + return request.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with async predicate', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(async request => { + return request.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .waitForRequest( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout', async () => { + const {page} = await getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForRequest(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with no timeout', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + page.waitForRequest(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(request.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForResponse', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png'), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should respect timeout', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .waitForResponse( + () => { + return false; + }, + {timeout: 1} + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default timeout', async () => { + const {page} = await getTestState(); + + let error!: Error; + page.setDefaultTimeout(1); + await page + .waitForResponse(() => { + return false; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should work with predicate', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(response => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with async predicate', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(async response => { + return response.url() === server.PREFIX + '/digits/2.png'; + }), + page.evaluate(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + it('should work with no timeout', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [response] = await Promise.all([ + page.waitForResponse(server.PREFIX + '/digits/2.png', {timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(response.url()).toBe(server.PREFIX + '/digits/2.png'); + }); + }); + + describe('Page.waitForNetworkIdle', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + let res; + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle().then(r => { + res = r; + return Date.now(); + }), + page + .evaluate(async () => { + await Promise.all([fetch('/digits/1.png'), fetch('/digits/2.png')]); + await new Promise(resolve => { + return setTimeout(resolve, 200); + }); + await fetch('/digits/3.png'); + await new Promise(resolve => { + return setTimeout(resolve, 200); + }); + await fetch('/digits/4.png'); + }) + .then(() => { + return Date.now(); + }), + ]); + expect(res).toBe(undefined); + expect(t1).toBeGreaterThan(t2); + expect(t1 - t2).toBeGreaterThanOrEqual(400); + }); + it('should respect timeout', async () => { + const {page} = await getTestState(); + let error!: Error; + await page.waitForNetworkIdle({timeout: 1}).catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect idleTime', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle({idleTime: 10}).then(() => { + return Date.now(); + }), + page + .evaluate(() => { + return (async () => { + await Promise.all([ + fetch('/digits/1.png'), + fetch('/digits/2.png'), + ]); + await new Promise(resolve => { + return setTimeout(resolve, 250); + }); + })(); + }) + .then(() => { + return Date.now(); + }), + ]); + expect(t2).toBeGreaterThan(t1); + }); + it('should work with no timeout', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + const [result] = await Promise.all([ + page.waitForNetworkIdle({timeout: 0}), + page.evaluate(() => { + return setTimeout(() => { + void fetch('/digits/1.png'); + void fetch('/digits/2.png'); + void fetch('/digits/3.png'); + }, 50); + }), + ]); + expect(result).toBe(undefined); + }); + it('should work with aborted requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/abort-request.html'); + + using element = await page.$(`#abort`); + await element!.click(); + + let error = false; + await page.waitForNetworkIdle().catch(() => { + return (error = true); + }); + + expect(error).toBe(false); + }); + it('should work with delayed response', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + let response!: ServerResponse; + server.setRoute('/fetch-request-b.js', (_req, res) => { + response = res; + }); + const t0 = Date.now(); + const [t1, t2] = await Promise.all([ + page.waitForNetworkIdle({idleTime: 100}).then(() => { + return Date.now(); + }), + new Promise<number>(res => { + setTimeout(() => { + response.end(); + res(Date.now()); + }, 300); + }), + page.evaluate(async () => { + await fetch('/fetch-request-b.js'); + }), + ]); + expect(t1).toBeGreaterThan(t2); + // request finished + idle time. + expect(t1 - t0).toBeGreaterThan(400); + // request finished + idle time - request finished. + expect(t1 - t2).toBeGreaterThanOrEqual(100); + }); + }); + + describe('Page.exposeFunction', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + const result = await page.evaluate(async function () { + return (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should throw exception in page context', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('woof', () => { + throw new Error('WOOF WOOF'); + }); + const {message, stack} = await page.evaluate(async () => { + try { + return await ( + globalThis as unknown as {woof(): Promise<never>} + ).woof(); + } catch (error) { + return { + message: (error as Error).message, + stack: (error as Error).stack, + }; + } + }); + expect(message).toBe('WOOF WOOF'); + expect(stack).toContain('page.spec.ts'); + }); + it('should support throwing "null"', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('woof', function () { + throw null; + }); + const thrown = await page.evaluate(async () => { + try { + await (globalThis as any).woof(); + return; + } catch (error) { + return error; + } + }); + expect(thrown).toBe(null); + }); + it('should be callable from-inside evaluateOnNewDocument', async () => { + const {page} = await getTestState(); + + let called = false; + await page.exposeFunction('woof', function () { + called = true; + }); + await page.evaluateOnNewDocument(() => { + return (globalThis as any).woof(); + }); + await page.reload(); + expect(called).toBe(true); + }); + it('should survive navigation', async () => { + const {page, server} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async function () { + return (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + it('should await returned promise', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + return (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should await returned if called from function', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + const result = await page.evaluate(async function () { + const result = await (globalThis as any).compute(3, 5); + return result; + }); + expect(result).toBe(15); + }); + it('should work on frames', async () => { + const {page, server} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + const frame = page.frames()[1]!; + const result = await frame.evaluate(async function () { + return (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work with loading frames', async () => { + // Tries to reproduce the scenario from + // https://github.com/puppeteer/puppeteer/issues/8106 + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + let saveRequest: (value: HTTPRequest | PromiseLike<HTTPRequest>) => void; + const iframeRequest = new Promise<HTTPRequest>(resolve => { + saveRequest = resolve; + }); + page.on('request', async req => { + if (req.url().endsWith('/frames/frame.html')) { + saveRequest(req); + } else { + await req.continue(); + } + }); + + let error: Error | undefined; + const navPromise = page + .goto(server.PREFIX + '/frames/one-frame.html', { + waitUntil: 'networkidle0', + }) + .catch(err => { + error = err; + }); + const req = await iframeRequest; + // Expose function while the frame is being loaded. Loading process is + // controlled by interception. + const exposePromise = page.exposeFunction( + 'compute', + function (a: number, b: number) { + return Promise.resolve(a * b); + } + ); + await Promise.all([req.continue(), exposePromise]); + await navPromise; + expect(error).toBeUndefined(); + const frame = page.frames()[1]!; + const result = await frame.evaluate(async function () { + return (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should work on frames before navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/frames/nested-frames.html'); + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + + const frame = page.frames()[1]!; + const result = await frame.evaluate(async function () { + return (globalThis as any).compute(3, 5); + }); + expect(result).toBe(15); + }); + it('should not throw when frames detach', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await attachFrame(page, 'frame1', server.EMPTY_PAGE); + await page.exposeFunction('compute', function (a: number, b: number) { + return Promise.resolve(a * b); + }); + await detachFrame(page, 'frame1'); + + await expect( + page.evaluate(async function () { + return (globalThis as any).compute(3, 5); + }) + ).resolves.toEqual(15); + }); + it('should work with complex objects', async () => { + const {page} = await getTestState(); + + await page.exposeFunction( + 'complexObject', + function (a: {x: number}, b: {x: number}) { + return {x: a.x + b.x}; + } + ); + const result = await page.evaluate(async () => { + return (globalThis as any).complexObject({x: 5}, {x: 2}); + }); + expect(result.x).toBe(7); + }); + it('should fallback to default export when passed a module object', async () => { + const {page, server} = await getTestState(); + const moduleObject = { + default: function (a: number, b: number) { + return a * b; + }, + }; + await page.goto(server.EMPTY_PAGE); + await page.exposeFunction('compute', moduleObject); + const result = await page.evaluate(async function () { + return (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + }); + }); + + describe('Page.removeExposedFunction', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.exposeFunction('compute', function (a: number, b: number) { + return a * b; + }); + const result = await page.evaluate(async function () { + return (globalThis as any).compute(9, 4); + }); + expect(result).toBe(36); + await page.removeExposedFunction('compute'); + + let error: Error | null = null; + await page + .evaluate(async function () { + return (globalThis as any).compute(9, 4); + }) + .catch(_error => { + return (error = _error); + }); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.Events.PageError', function () { + it('should fire', async () => { + const {page, server} = await getTestState(); + + const [error] = await Promise.all([ + waitEvent<Error>(page, 'pageerror', err => { + return err.message.includes('Fancy'); + }), + page.goto(server.PREFIX + '/error.html'), + ]); + expect(error.message).toContain('Fancy'); + expect(error.stack?.split('\n')[1]).toContain('error.html:13'); + }); + }); + + describe('Page.setUserAgent', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('Mozilla'); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should work for subframes', async () => { + const {page, server} = await getTestState(); + + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('Mozilla'); + await page.setUserAgent('foobar'); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + attachFrame(page, 'frame1', server.EMPTY_PAGE), + ]); + expect(request.headers['user-agent']).toBe('foobar'); + }); + it('should emulate device user-agent', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/mobile.html'); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).not.toContain('iPhone'); + await page.setUserAgent(KnownDevices['iPhone 6'].userAgent); + expect( + await page.evaluate(() => { + return navigator.userAgent; + }) + ).toContain('iPhone'); + }); + it('should work with additional userAgentMetdata', async () => { + const {page, server} = await getTestState(); + + await page.setUserAgent('MockBrowser', { + architecture: 'Mock1', + mobile: false, + model: 'Mockbook', + platform: 'MockOS', + platformVersion: '3.1', + }); + const [request] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect( + await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error: userAgentData not yet in TypeScript DOM API + return navigator.userAgentData.mobile; + }) + ).toBe(false); + + const uaData = await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error: userAgentData not yet in TypeScript DOM API + return navigator.userAgentData.getHighEntropyValues([ + 'architecture', + 'model', + 'platform', + 'platformVersion', + ]); + }); + expect(uaData['architecture']).toBe('Mock1'); + expect(uaData['model']).toBe('Mockbook'); + expect(uaData['platform']).toBe('MockOS'); + expect(uaData['platformVersion']).toBe('3.1'); + expect(request.headers['user-agent']).toBe('MockBrowser'); + }); + }); + + describe('Page.setContent', function () { + const expectedOutput = + '<html><head></head><body><div>hello</div></body></html>'; + it('should work', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>hello</div>'); + const result = await page.content(); + expect(result).toBe(expectedOutput); + }); + it('should work with doctype', async () => { + const {page} = await getTestState(); + + const doctype = '<!DOCTYPE html>'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should work with HTML 4 doctype', async () => { + const {page} = await getTestState(); + + const doctype = + '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" ' + + '"http://www.w3.org/TR/html4/strict.dtd">'; + await page.setContent(`${doctype}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${doctype}${expectedOutput}`); + }); + it('should respect timeout', async () => { + const {page, server} = await getTestState(); + + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`, { + timeout: 1, + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should respect default navigation timeout', async () => { + const {page, server} = await getTestState(); + + page.setDefaultNavigationTimeout(1); + const imgPath = '/img.png'; + // stall for image + server.setRoute(imgPath, () => {}); + let error!: Error; + await page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + it('should await resources to load', async () => { + const {page, server} = await getTestState(); + + const imgPath = '/img.png'; + let imgResponse!: ServerResponse; + server.setRoute(imgPath, (_req, res) => { + return (imgResponse = res); + }); + let loaded = false; + const contentPromise = page + .setContent(`<img src="${server.PREFIX + imgPath}"></img>`) + .then(() => { + return (loaded = true); + }); + await server.waitForRequest(imgPath); + expect(loaded).toBe(false); + imgResponse.end(); + await contentPromise; + }); + it('should work fast enough', async () => { + const {page} = await getTestState(); + + for (let i = 0; i < 20; ++i) { + await page.setContent('<div>yo</div>'); + } + }); + it('should work with tricky content', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>hello world</div>' + '\x7F'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('hello world'); + }); + it('should work with accents', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>aberración</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('aberración'); + }); + it('should work with emojis', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>🐥</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('🐥'); + }); + it('should work with newline', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>\n</div>'); + expect( + await page.$eval('div', div => { + return div.textContent; + }) + ).toBe('\n'); + }); + it('should work with comments outside HTML tag', async () => { + const {page} = await getTestState(); + + const comment = '<!-- Comment -->'; + await page.setContent(`${comment}<div>hello</div>`); + const result = await page.content(); + expect(result).toBe(`${comment}${expectedOutput}`); + }); + }); + + describe('Page.setBypassCSP', function () { + it('should bypass CSP meta tag', async () => { + const {page, server} = await getTestState(); + + // Make sure CSP prohibits addScriptTag. + await page.goto(server.PREFIX + '/csp.html'); + await page + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + }); + + it('should bypass CSP header', async () => { + const {page, server} = await getTestState(); + + // Make sure CSP prohibits addScriptTag. + server.setCSP('/empty.html', 'default-src "self"'); + await page.goto(server.EMPTY_PAGE); + await page + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(undefined); + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + }); + + it('should bypass after cross-process navigation', async () => { + const {page, server} = await getTestState(); + + await page.setBypassCSP(true); + await page.goto(server.PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + + await page.goto(server.CROSS_PROCESS_PREFIX + '/csp.html'); + await page.addScriptTag({content: 'window.__injected = 42;'}); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + }); + it('should bypass CSP in iframes as well', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + { + // Make sure CSP prohibits addScriptTag in an iframe. + const frame = (await attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ))!; + await frame + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await frame.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(undefined); + } + + // By-pass CSP and try one more time. + await page.setBypassCSP(true); + await page.reload(); + + { + const frame = (await attachFrame( + page, + 'frame1', + server.PREFIX + '/csp.html' + ))!; + await frame + .addScriptTag({content: 'window.__injected = 42;'}) + .catch(error => { + return void error; + }); + expect( + await frame.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + } + }); + }); + + describe('Page.addScriptTag', function () { + it('should throw an error if no options are provided', async () => { + const {page} = await getTestState(); + + let error!: Error; + try { + // @ts-expect-error purposefully passing bad options + await page.addScriptTag('/injectedfile.js'); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + }); + + it('should work with a url', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using scriptHandle = await page.addScriptTag({url: '/injectedfile.js'}); + expect(scriptHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + }); + + it('should work with a url and type=module', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({url: '/es6/es6import.js', type: 'module'}); + expect( + await page.evaluate(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }) + ).toBe(42); + }); + + it('should work with a path and type=module', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, '../assets/es6/es6pathimport.js'), + type: 'module', + }); + await page.waitForFunction(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }); + expect( + await page.evaluate(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }) + ).toBe(42); + }); + + it('should work with a content and type=module', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + content: `import num from '/es6/es6module.js';window.__es6injected = num;`, + type: 'module', + }); + await page.waitForFunction(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }); + expect( + await page.evaluate(() => { + return (window as unknown as {__es6injected: number}).__es6injected; + }) + ).toBe(42); + }); + + it('should throw an error if loading from url fail', async () => { + const {page, server, isFirefox} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error!: Error; + try { + await page.addScriptTag({url: '/nonexistfile.js'}); + } catch (error_) { + error = error_ as Error; + } + if (isFirefox) { + expect(error.message).toBeTruthy(); + } else { + expect(error.message).toContain('Could not load script'); + } + }); + + it('should work with a path', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using scriptHandle = await page.addScriptTag({ + path: path.join(__dirname, '../assets/injectedfile.js'), + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(42); + }); + + it('should include sourcemap when path is provided', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({ + path: path.join(__dirname, '../assets/injectedfile.js'), + }); + const result = await page.evaluate(() => { + return (globalThis as any).__injectedError.stack; + }); + expect(result).toContain(path.join('assets', 'injectedfile.js')); + }); + + it('should work with content', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using scriptHandle = await page.addScriptTag({ + content: 'window.__injected = 35;', + }); + expect(scriptHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate(() => { + return (globalThis as any).__injected; + }) + ).toBe(35); + }); + + it('should add id when provided', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.addScriptTag({content: 'window.__injected = 1;', id: 'one'}); + await page.addScriptTag({url: '/injectedfile.js', id: 'two'}); + expect(await page.$('#one')).not.toBeNull(); + expect(await page.$('#two')).not.toBeNull(); + }); + + // @see https://github.com/puppeteer/puppeteer/issues/4840 + it('should throw when added with content to the CSP page', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addScriptTag({content: 'window.__injected = 35;'}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addScriptTag({url: server.CROSS_PROCESS_PREFIX + '/injectedfile.js'}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.addStyleTag', function () { + it('should throw an error if no options are provided', async () => { + const {page} = await getTestState(); + + let error!: Error; + try { + // @ts-expect-error purposefully passing bad input + await page.addStyleTag('/injectedstyle.css'); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toBe( + 'Exactly one of `url`, `path`, or `content` must be specified.' + ); + }); + + it('should work with a url', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using styleHandle = await page.addStyleTag({url: '/injectedstyle.css'}); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should throw an error if loading from url fail', async () => { + const {page, server, isFirefox} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let error!: Error; + try { + await page.addStyleTag({url: '/nonexistfile.js'}); + } catch (error_) { + error = error_ as Error; + } + if (isFirefox) { + expect(error.message).toBeTruthy(); + } else { + expect(error.message).toContain('Could not load style'); + } + }); + + it('should work with a path', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using styleHandle = await page.addStyleTag({ + path: path.join(__dirname, '../assets/injectedstyle.css'), + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(255, 0, 0)'); + }); + + it('should include sourcemap when path is provided', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.addStyleTag({ + path: path.join(__dirname, '../assets/injectedstyle.css'), + }); + using styleHandle = (await page.$('style'))!; + const styleContent = await page.evaluate((style: HTMLStyleElement) => { + return style.innerHTML; + }, styleHandle); + expect(styleContent).toContain(path.join('assets', 'injectedstyle.css')); + }); + + it('should work with content', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + using styleHandle = await page.addStyleTag({ + content: 'body { background-color: green; }', + }); + expect(styleHandle.asElement()).not.toBeNull(); + expect( + await page.evaluate( + `window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')` + ) + ).toBe('rgb(0, 128, 0)'); + }); + + it('should throw when added with content to the CSP page', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addStyleTag({content: 'body { background-color: green; }'}) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + }); + + it('should throw when added with URL to the CSP page', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/csp.html'); + let error!: Error; + await page + .addStyleTag({ + url: server.CROSS_PROCESS_PREFIX + '/injectedstyle.css', + }) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + }); + }); + + describe('Page.url', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + expect(page.url()).toBe('about:blank'); + await page.goto(server.EMPTY_PAGE); + expect(page.url()).toBe(server.EMPTY_PAGE); + }); + }); + + describe('Page.setJavaScriptEnabled', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setJavaScriptEnabled(false); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + let error!: Error; + await page.evaluate('something').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('something is not defined'); + + await page.setJavaScriptEnabled(true); + await page.goto( + 'data:text/html, <script>var something = "forbidden"</script>' + ); + expect(await page.evaluate('something')).toBe('forbidden'); + }); + }); + + describe('Page.setCacheEnabled', function () { + it('should enable or disable the cache based on the state passed', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [cachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + // Rely on "if-modified-since" caching in our test server. + expect(cachedRequest.headers['if-modified-since']).not.toBe(undefined); + + await page.setCacheEnabled(false); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + it('should stay disabled when toggling request interception on/off', async () => { + const {page, server} = await getTestState(); + + await page.setCacheEnabled(false); + await page.setRequestInterception(true); + await page.setRequestInterception(false); + + await page.goto(server.PREFIX + '/cached/one-style.html'); + const [nonCachedRequest] = await Promise.all([ + server.waitForRequest('/cached/one-style.html'), + page.reload(), + ]); + expect(nonCachedRequest.headers['if-modified-since']).toBe(undefined); + }); + }); + + describe('Page.pdf', function () { + it('can print to PDF and save to file', async () => { + const {page, server} = await getTestState(); + + const outputFile = __dirname + '/../assets/output.pdf'; + await page.goto(server.PREFIX + '/pdf.html'); + await page.pdf({path: outputFile}); + try { + expect(fs.readFileSync(outputFile).byteLength).toBeGreaterThan(0); + } finally { + fs.unlinkSync(outputFile); + } + }); + + it('can print to PDF with accessible', async () => { + const {page, server} = await getTestState(); + + const outputFile = __dirname + '/../assets/output.pdf'; + const outputFileAccessible = + __dirname + '/../assets/output-accessible.pdf'; + await page.goto(server.PREFIX + '/pdf.html'); + await page.pdf({path: outputFile}); + await page.pdf({path: outputFileAccessible, tagged: true}); + try { + expect( + fs.readFileSync(outputFileAccessible).byteLength + ).toBeGreaterThan(fs.readFileSync(outputFile).byteLength); + } finally { + fs.unlinkSync(outputFileAccessible); + fs.unlinkSync(outputFile); + } + }); + + it('can print to PDF and stream the result', async () => { + const {page} = await getTestState(); + + const stream = await page.createPDFStream(); + let size = 0; + for await (const chunk of stream) { + size += chunk.length; + } + expect(size).toBeGreaterThan(0); + }); + + it('should respect timeout', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/pdf.html'); + + const error = await page.pdf({timeout: 1}).catch(err => { + return err; + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + }); + + describe('Page.title', function () { + it('should return the page title', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/title.html'); + expect(await page.title()).toBe('Woof-Woof'); + }); + }); + + describe('Page.select', function () { + it('should select single option', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onChange; + }) + ).toEqual(['blue']); + }); + it('should select only first option', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'green', 'red'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onChange; + }) + ).toEqual(['blue']); + }); + it('should not throw when select causes navigation', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.$eval('select', select => { + return select.addEventListener('input', () => { + return ((window as any).location = '/empty.html'); + }); + }); + await Promise.all([ + page.select('select', 'blue'), + page.waitForNavigation(), + ]); + expect(page.url()).toContain('empty.html'); + }); + it('should select multiple options', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => { + return (globalThis as any).makeMultiple(); + }); + await page.select('select', 'blue', 'green', 'red'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue', 'green', 'red']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onChange; + }) + ).toEqual(['blue', 'green', 'red']); + }); + it('should respect event bubbling', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onBubblingInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onBubblingChange; + }) + ).toEqual(['blue']); + }); + it('should throw when element is not a <select>', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('body', '').catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain('Element is not a <select> element.'); + }); + it('should return [] on no matched values', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select', '42', 'abc'); + expect(result).toEqual([]); + }); + it('should return an array of matched values', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => { + return (globalThis as any).makeMultiple(); + }); + const result = await page.select('select', 'blue', 'black', 'magenta'); + expect( + result.reduce((accumulator, current) => { + return ['blue', 'black', 'magenta'].includes(current) && accumulator; + }, true) + ).toEqual(true); + }); + it('should return an array of one element when multiple is not set', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select( + 'select', + '42', + 'blue', + 'black', + 'magenta' + ); + expect(result).toHaveLength(1); + }); + it('should return [] on no values', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + const result = await page.select('select'); + expect(result).toEqual([]); + }); + it('should deselect all options when passed no values for a multiple select', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => { + return (globalThis as any).makeMultiple(); + }); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', select => { + return Array.from((select as HTMLSelectElement).options).every( + option => { + return !option.selected; + } + ); + }) + ).toEqual(true); + }); + it('should deselect all options when passed no values for a select without multiple', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.select('select', 'blue', 'black', 'magenta'); + await page.select('select'); + expect( + await page.$eval('select', select => { + return Array.from((select as HTMLSelectElement).options).filter( + option => { + return option.selected; + } + )[0]!.value; + }) + ).toEqual(''); + }); + it('should throw if passed in non-strings', async () => { + const {page} = await getTestState(); + + await page.setContent('<select><option value="12"/></select>'); + let error!: Error; + try { + // @ts-expect-error purposefully passing bad input + await page.select('select', 12); + } catch (error_) { + error = error_ as Error; + } + expect(error.message).toContain('Values must be strings'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3327 + it('should work when re-defining top-level Event class', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/input/select.html'); + await page.evaluate(() => { + // @ts-expect-error Expected. + return (window.Event = undefined); + }); + await page.select('select', 'blue'); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onInput; + }) + ).toEqual(['blue']); + expect( + await page.evaluate(() => { + return (globalThis as any).result.onChange; + }) + ).toEqual(['blue']); + }); + }); + + describe('Page.Events.Close', function () { + it('should work with window.close', async () => { + const {page, context} = await getTestState(); + + const newPagePromise = new Promise<Page | null>(fulfill => { + return context.once('targetcreated', target => { + return fulfill(target.page()); + }); + }); + assert(page); + await page.evaluate(() => { + return ((window as any)['newPage'] = window.open('about:blank')); + }); + const newPage = await newPagePromise; + assert(newPage); + const closedPromise = waitEvent(newPage, 'close'); + await page.evaluate(() => { + return (window as any)['newPage'].close(); + }); + await closedPromise; + }); + it('should work with page.close', async () => { + const {context} = await getTestState(); + + const newPage = await context.newPage(); + const closedPromise = waitEvent(newPage, 'close'); + await newPage.close(); + await closedPromise; + }); + }); + + describe('Page.browser', function () { + it('should return the correct browser instance', async () => { + const {page, browser} = await getTestState(); + + expect(page.browser()).toBe(browser); + }); + }); + + describe('Page.browserContext', function () { + it('should return the correct browser context instance', async () => { + const {page, context} = await getTestState(); + + expect(page.browserContext()).toBe(context); + }); + }); + + describe('Page.client', function () { + it('should return the client instance', async () => { + const {page} = await getTestState(); + expect((page as CdpPage)._client()).toBeInstanceOf(CDPSession); + }); + }); + + describe('Page.bringToFront', function () { + it('should work', async () => { + const {browser} = await getTestState(); + const page1 = await browser.newPage(); + const page2 = await browser.newPage(); + + await page1.bringToFront(); + expect( + await page1.evaluate(() => { + return document.visibilityState; + }) + ).toBe('visible'); + expect( + await page2.evaluate(() => { + return document.visibilityState; + }) + ).toBe('hidden'); + + await page2.bringToFront(); + expect( + await page1.evaluate(() => { + return document.visibilityState; + }) + ).toBe('hidden'); + expect( + await page2.evaluate(() => { + return document.visibilityState; + }) + ).toBe('visible'); + + await page1.close(); + await page2.close(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/proxy.spec.ts b/remote/test/puppeteer/test/src/proxy.spec.ts new file mode 100644 index 0000000000..07b73cdd0d --- /dev/null +++ b/remote/test/puppeteer/test/src/proxy.spec.ts @@ -0,0 +1,236 @@ +/** + * @license + * Copyright 2021 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {IncomingMessage, Server, ServerResponse} from 'http'; +import http from 'http'; +import type {AddressInfo} from 'net'; +import os from 'os'; + +import type {TestServer} from '@pptr/testserver'; +import expect from 'expect'; + +import {getTestState, launch} from './mocha-utils.js'; + +let HOSTNAME = os.hostname(); + +// Hostname might not be always accessible in environments other than GitHub +// Actions. Therefore, we try to find an external IPv4 address to be used as a +// hostname in these tests. +const networkInterfaces = os.networkInterfaces(); +for (const key of Object.keys(networkInterfaces)) { + const interfaces = networkInterfaces[key]; + for (const net of interfaces || []) { + if (net.family === 'IPv4' && !net.internal) { + HOSTNAME = net.address; + break; + } + } +} + +/** + * Requests to localhost do not get proxied by default. Create a URL using the hostname + * instead. + */ +function getEmptyPageUrl(server: TestServer): string { + const emptyPagePath = new URL(server.EMPTY_PAGE).pathname; + + return `http://${HOSTNAME}:${server.PORT}${emptyPagePath}`; +} + +describe('request proxy', () => { + let proxiedRequestUrls: string[]; + let proxyServer: Server; + let proxyServerUrl: string; + const defaultArgs = [ + // We disable this in tests so that proxy-related tests + // don't intercept queries from this service in headful. + '--disable-features=NetworkTimeServiceQuerying', + ]; + + beforeEach(() => { + proxiedRequestUrls = []; + + proxyServer = http + .createServer( + ( + originalRequest: IncomingMessage, + originalResponse: ServerResponse + ) => { + proxiedRequestUrls.push(originalRequest.url as string); + + const proxyRequest = http.request( + originalRequest.url as string, + { + method: originalRequest.method, + headers: originalRequest.headers, + }, + proxyResponse => { + originalResponse.writeHead( + proxyResponse.statusCode as number, + proxyResponse.headers + ); + proxyResponse.pipe(originalResponse, {end: true}); + } + ); + + originalRequest.pipe(proxyRequest, {end: true}); + } + ) + .listen(); + + proxyServerUrl = `http://${HOSTNAME}:${ + (proxyServer.address() as AddressInfo).port + }`; + }); + + afterEach(async () => { + await new Promise((resolve, reject) => { + proxyServer.close(error => { + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); + }); + }); + + it('should proxy requests when configured', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + try { + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + } finally { + await close(); + } + }); + + it('should respect proxy bypass list', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + try { + const page = await browser.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([]); + } finally { + await close(); + } + }); + + describe('in incognito browser context', () => { + it('should proxy requests when configured at browser level', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: [...defaultArgs, `--proxy-server=${proxyServerUrl}`], + }); + try { + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + } finally { + await close(); + } + }); + + it('should respect proxy bypass list when configured at browser level', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: [ + ...defaultArgs, + `--proxy-server=${proxyServerUrl}`, + `--proxy-bypass-list=${new URL(emptyPageUrl).host}`, + ], + }); + try { + const context = await browser.createIncognitoBrowserContext(); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([]); + } finally { + await close(); + } + }); + + /** + * See issues #7873, #7719, and #7698. + */ + it('should proxy requests when configured at context level', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: defaultArgs, + }); + try { + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + }); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([emptyPageUrl]); + } finally { + await close(); + } + }); + + it('should respect proxy bypass list when configured at context level', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const emptyPageUrl = getEmptyPageUrl(server); + const {browser, close} = await launch({ + args: defaultArgs, + }); + try { + const context = await browser.createIncognitoBrowserContext({ + proxyServer: proxyServerUrl, + proxyBypassList: [new URL(emptyPageUrl).host], + }); + const page = await context.newPage(); + const response = (await page.goto(emptyPageUrl))!; + + expect(response.ok()).toBe(true); + expect(proxiedRequestUrls).toEqual([]); + } finally { + await close(); + } + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/queryhandler.spec.ts b/remote/test/puppeteer/test/src/queryhandler.spec.ts new file mode 100644 index 0000000000..05f201a9be --- /dev/null +++ b/remote/test/puppeteer/test/src/queryhandler.spec.ts @@ -0,0 +1,653 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import assert from 'assert'; + +import expect from 'expect'; +import {Puppeteer} from 'puppeteer-core'; +import type {ElementHandle} from 'puppeteer-core/internal/api/ElementHandle.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('Query handler tests', function () { + setupTestBrowserHooks(); + + describe('Pierce selectors', function () { + async function setUpPage(): ReturnType<typeof getTestState> { + const state = await getTestState(); + await state.page.setContent( + `<script> + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({mode: 'open'}); + const div1 = document.createElement('div'); + div1.textContent = 'Hello'; + div1.className = 'foo'; + const div2 = document.createElement('div'); + div2.textContent = 'World'; + div2.className = 'foo'; + shadowRoot.appendChild(div1); + shadowRoot.appendChild(div2); + document.documentElement.appendChild(div); + </script>` + ); + return state; + } + it('should find first element in shadow', async () => { + const {page} = await setUpPage(); + using div = (await page.$('pierce/.foo')) as ElementHandle<HTMLElement>; + const text = await div.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all elements in shadow', async () => { + const {page} = await setUpPage(); + const divs = (await page.$$('pierce/.foo')) as Array< + ElementHandle<HTMLElement> + >; + const text = await Promise.all( + divs.map(div => { + return div.evaluate(element => { + return element.textContent; + }); + }) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + it('should find first child element', async () => { + const {page} = await setUpPage(); + using parentElement = (await page.$('html > div'))!; + using childElement = (await parentElement.$( + 'pierce/div' + )) as ElementHandle<HTMLElement>; + const text = await childElement.evaluate(element => { + return element.textContent; + }); + expect(text).toBe('Hello'); + }); + it('should find all child elements', async () => { + const {page} = await setUpPage(); + using parentElement = (await page.$('html > div'))!; + const childElements = (await parentElement.$$('pierce/div')) as Array< + ElementHandle<HTMLElement> + >; + const text = await Promise.all( + childElements.map(div => { + return div.evaluate(element => { + return element.textContent; + }); + }) + ); + expect(text.join(' ')).toBe('Hello World'); + }); + }); + + describe('Text selectors', function () { + describe('in Page', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>test</section>'); + + expect(await page.$('text/test')).toBeTruthy(); + expect(await page.$$('text/test')).toHaveLength(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = await getTestState(); + + expect(await page.$('text/test')).toBeFalsy(); + expect(await page.$$('text/test')).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div id="1">a</div><div>a</div>'); + + using element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.id; + }) + ).toBe('1'); + }); + it('should return multiple elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>a</div><div>a</div>'); + + const elements = await page.$$('text/a'); + expect(elements).toHaveLength(2); + }); + it('should pierce shadow DOM', async () => { + const {page} = await getTestState(); + + await page.evaluate(() => { + const div = document.createElement('div'); + const shadow = div.attachShadow({mode: 'open'}); + const diva = document.createElement('div'); + shadow.append(diva); + const divb = document.createElement('div'); + shadow.append(divb); + diva.innerHTML = 'a'; + divb.innerHTML = 'b'; + document.body.append(div); + }); + + using element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query deeply nested text', async () => { + const {page} = await getTestState(); + + await page.setContent('<div><div>a</div><div>b</div></div>'); + + using element = await page.$('text/a'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a'); + }); + it('should query inputs', async () => { + const {page} = await getTestState(); + + await page.setContent('<input value="a">'); + + using element = (await page.$( + 'text/a' + )) as ElementHandle<HTMLInputElement>; + expect( + await element?.evaluate(e => { + return e.value; + }) + ).toBe('a'); + }); + it('should not query radio', async () => { + const {page} = await getTestState(); + + await page.setContent('<radio value="a">'); + + expect(await page.$('text/a')).toBeNull(); + }); + it('should query text spanning multiple elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div><span>a</span> <span>b</span><div>'); + + using element = await page.$('text/a b'); + expect( + await element?.evaluate(e => { + return e.textContent; + }) + ).toBe('a b'); + }); + it('should clear caches', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div id=target1>text</div><input id=target2 value=text><div id=target3>text</div>' + ); + using div = (await page.$('#target1')) as ElementHandle<HTMLDivElement>; + using input = (await page.$( + '#target2' + )) as ElementHandle<HTMLInputElement>; + + await div.evaluate(div => { + div.textContent = 'text'; + }); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target1'); + await div.evaluate(div => { + div.textContent = 'foo'; + }); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target2'); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('foo'); + expect( + await page.$eval(`text/text`, e => { + return e.id; + }) + ).toBe('target3'); + + await div.evaluate(div => { + div.textContent = 'text'; + }); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('text'); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(3); + await div.evaluate(div => { + div.textContent = 'foo'; + }); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(2); + await input.evaluate(input => { + input.value = ''; + }); + await input.type('foo'); + expect( + await page.$$eval(`text/text`, es => { + return es.length; + }) + ).toBe(1); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div class="a"><span>a</span></div>'); + + using elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`text/a`)).toBeTruthy(); + expect(await elementHandle.$$(`text/a`)).toHaveLength(1); + }); + + it('should return null for non-existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div class="a"></div>'); + + using elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`text/a`)).toBeFalsy(); + expect(await elementHandle.$$(`text/a`)).toHaveLength(0); + }); + }); + }); + + describe('XPath selectors', function () { + describe('in Page', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>test</section>'); + + expect(await page.$('xpath/html/body/section')).toBeTruthy(); + expect(await page.$$('xpath/html/body/section')).toHaveLength(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = await getTestState(); + + expect( + await page.$('xpath/html/body/non-existing-element') + ).toBeFalsy(); + expect( + await page.$$('xpath/html/body/non-existing-element') + ).toHaveLength(0); + }); + it('should return first element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>a</div><div></div>'); + + using element = await page.$('xpath/html/body/div'); + expect( + await element?.evaluate(e => { + return e.textContent === 'a'; + }) + ).toBeTruthy(); + }); + it('should return multiple elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div></div><div></div>'); + + const elements = await page.$$('xpath/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + describe('in ElementHandles', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div class="a">a<span></span></div>'); + + using elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeTruthy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(1); + }); + + it('should return null for non-existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div class="a">a</div>'); + + using elementHandle = (await page.$('div'))!; + expect(await elementHandle.$(`xpath/span`)).toBeFalsy(); + expect(await elementHandle.$$(`xpath/span`)).toHaveLength(0); + }); + }); + }); + + describe('P selectors', () => { + beforeEach(async () => { + Puppeteer.clearCustomQueryHandlers(); + }); + + it('should work with CSS selectors', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('div > button'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + + // Should parse more complex CSS selectors. Listing a few problematic + // cases from bug reports. + for (const selector of [ + '.user_row[data-user-id="\\38 "]:not(.deactivated_user)', + `input[value='Search']:not([class='hidden'])`, + `[data-test-id^="test-"]:not([data-test-id^="test-foo"])`, + ]) { + await page.$$(selector); + } + }); + + it('should work with deep combinators', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + { + using element = await page.$('div >>>> div'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'c'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('div >>> div'); + assert(elements[1], 'Could not find element'); + expect( + await elements[1]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('#c >>>> div'); + assert(elements[0], 'Could not find element'); + expect( + await elements[0]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + { + const elements = await page.$$('#c >>> div'); + assert(elements[0], 'Could not find element'); + expect( + await elements[0]?.evaluate(element => { + return element.id === 'd'; + }) + ).toBeTruthy(); + } + }); + + it('should work with text selectors', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('div ::-p-text(world)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work ARIA selectors', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('div ::-p-aria(world)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work for ARIA selectors in multiple isolated worlds', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.waitForSelector('::-p-aria(world)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + // $ would add ARIA query handler to the main world. + await element.$('::-p-aria(world)'); + using element2 = await page.waitForSelector('::-p-aria(world)'); + assert(element2, 'Could not find element'); + expect( + await element2.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work ARIA selectors with role', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('::-p-aria(world[role="button"])'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work ARIA selectors with name and role', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('::-p-aria([name="world"][role="button"])'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work XPath selectors', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('div ::-p-xpath(//button)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + }); + + it('should work with custom selectors', async () => { + Puppeteer.registerCustomQueryHandler('div', { + queryOne() { + return document.querySelector('div'); + }, + }); + + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$('::-p-div'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + }); + + it('should work with custom selectors with args', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + Puppeteer.registerCustomQueryHandler('div', { + queryOne(_, selector) { + if (selector === 'true') { + return document.querySelector('div'); + } else { + return document.querySelector('button'); + } + }, + }); + + { + using element = await page.$('::-p-div(true)'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + using element = await page.$('::-p-div("true")'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + using element = await page.$("::-p-div('true')"); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'a'; + }) + ).toBeTruthy(); + } + { + using element = await page.$('::-p-div'); + assert(element, 'Could not find element'); + expect( + await element.evaluate(element => { + return element.id === 'b'; + }) + ).toBeTruthy(); + } + }); + + it('should work with :hover', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using button = await page.$('div ::-p-text(world)'); + assert(button, 'Could not find element'); + await button.hover(); + + using button2 = await page.$('div ::-p-text(world):hover'); + assert(button2, 'Could not find element'); + const value = await button2.evaluate(span => { + return {textContent: span.textContent, tagName: span.tagName}; + }); + expect(value).toMatchObject({textContent: 'world', tagName: 'BUTTON'}); + }); + + it('should work with selector lists', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + const elements = await page.$$('div, ::-p-text(world)'); + expect(elements).toHaveLength(3); + }); + + const permute = <T>(inputs: T[]): T[][] => { + const results: T[][] = []; + for (let i = 0; i < inputs.length; ++i) { + const permutation = permute( + inputs.slice(0, i).concat(inputs.slice(i + 1)) + ); + const value = inputs[i] as T; + if (permutation.length === 0) { + results.push([value]); + continue; + } + for (const part of permutation) { + results.push([value].concat(part)); + } + } + return results; + }; + + it('should match querySelector* ordering', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + for (const list of permute(['div', 'button', 'span'])) { + const elements = await page.$$( + list + .map(selector => { + return selector === 'button' ? '::-p-text(world)' : selector; + }) + .join(',') + ); + const actual = await Promise.all( + elements.map(element => { + return element.evaluate(element => { + return element.id; + }); + }) + ); + expect(actual.join()).toStrictEqual('a,b,f,c'); + } + }); + + it('should not have duplicate elements from selector lists', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + const elements = await page.$$('::-p-text(world), button'); + expect(elements).toHaveLength(1); + }); + + it('should handle escapes', async () => { + const {server, page} = await getTestState(); + await page.goto(`${server.PREFIX}/p-selectors.html`); + using element = await page.$( + ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\))' + ); + expect(element).toBeTruthy(); + using element2 = await page.$( + ':scope >>> ::-p-text("My name is Jun (pronounced like \\"June\\")")' + ); + expect(element2).toBeTruthy(); + using element3 = await page.$( + ':scope >>> ::-p-text(My name is Jun \\(pronounced like "June"\\)")' + ); + expect(element3).toBeFalsy(); + using element4 = await page.$( + ':scope >>> ::-p-text("My name is Jun \\(pronounced like "June"\\))' + ); + expect(element4).toBeFalsy(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/queryselector.spec.ts b/remote/test/puppeteer/test/src/queryselector.spec.ts new file mode 100644 index 0000000000..7fd27f914f --- /dev/null +++ b/remote/test/puppeteer/test/src/queryselector.spec.ts @@ -0,0 +1,491 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ +import expect from 'expect'; +import {Puppeteer} from 'puppeteer'; +import type {CustomQueryHandler} from 'puppeteer-core/internal/common/CustomQueryHandler.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +describe('querySelector', function () { + setupTestBrowserHooks(); + + describe('Page.$eval', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setContent('<section id="testAttribute">43543</section>'); + const idAttribute = await page.$eval('section', e => { + return e.id; + }); + expect(idAttribute).toBe('testAttribute'); + }); + it('should accept arguments', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>hello</section>'); + const text = await page.$eval( + 'section', + (e, suffix) => { + return e.textContent! + suffix; + }, + ' world!' + ); + expect(text).toBe('hello world!'); + }); + it('should accept ElementHandles as arguments', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>hello</section><div> world</div>'); + using divHandle = (await page.$('div'))!; + const text = await page.$eval( + 'section', + (e, div) => { + return e.textContent! + (div as HTMLElement).textContent!; + }, + divHandle + ); + expect(text).toBe('hello world'); + }); + it('should throw error if no element is found', async () => { + const {page} = await getTestState(); + + let error!: Error; + await page + .$eval('section', e => { + return e.id; + }) + .catch(error_ => { + return (error = error_); + }); + expect(error.message).toContain( + 'failed to find element matching selector "section"' + ); + }); + }); + + // The tests for $$eval are repeated later in this file in the test group 'QueryAll'. + // This is done to also test a query handler where QueryAll returns an Element[] + // as opposed to NodeListOf<Element>. + describe('Page.$$eval', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval('div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('should accept extra arguments', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'div', + (divs, two, three) => { + return divs.length + (two as number) + (three as number); + }, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('should accept ElementHandles as arguments', async () => { + const {page} = await getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + using divHandle = (await page.$('div'))!; + const sum = await page.$$eval( + 'section', + (sections, div) => { + return ( + sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0) + Number((div as HTMLElement).textContent) + ); + }, + divHandle + ); + expect(sum).toBe(8); + }); + it('should handle many elements', async function () { + this.timeout(25_000); + + const {page} = await getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('section', sections => { + return sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0); + }); + expect(sum).toBe(500500); + }); + }); + + describe('Page.$', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>test</section>'); + using element = (await page.$('section'))!; + expect(element).toBeTruthy(); + }); + it('should return null for non-existing element', async () => { + const {page} = await getTestState(); + + using element = (await page.$('non-existing-element'))!; + expect(element).toBe(null); + }); + }); + + describe('Page.$$', function () { + it('should query existing elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div>A</div><br/><div>B</div>'); + const elements = await page.$$('div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate((e: HTMLElement) => { + return e.textContent; + }, element); + }); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + it('should return empty array if nothing is found', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const elements = await page.$$('div'); + expect(elements).toHaveLength(0); + }); + }); + + describe('Page.$x', function () { + it('should query existing element', async () => { + const {page} = await getTestState(); + + await page.setContent('<section>test</section>'); + const elements = await page.$x('/html/body/section'); + expect(elements[0]).toBeTruthy(); + expect(elements).toHaveLength(1); + }); + it('should return empty array for non-existing element', async () => { + const {page} = await getTestState(); + + const element = await page.$x('/html/body/non-existing-element'); + expect(element).toEqual([]); + }); + it('should return multiple elements', async () => { + const {page} = await getTestState(); + + await page.setContent('<div></div><div></div>'); + const elements = await page.$x('/html/body/div'); + expect(elements).toHaveLength(2); + }); + }); + + describe('ElementHandle.$', function () { + it('should query existing element', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + using html = (await page.$('html'))!; + using second = (await html.$('.second'))!; + using inner = await second.$('.inner'); + const content = await page.evaluate(e => { + return e?.textContent; + }, inner); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + using html = (await page.$('html'))!; + using second = await html.$('.third'); + expect(second).toBe(null); + }); + }); + describe('ElementHandle.$eval', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div class="tweet"><div class="like">100</div><div class="retweets">10</div></div></body></html>' + ); + using tweet = (await page.$('.tweet'))!; + const content = await tweet.$eval('.like', node => { + return (node as HTMLElement).innerText; + }); + expect(content).toBe('100'); + }); + + it('should retrieve content from subtree', async () => { + const {page} = await getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a-child-div</div></div>'; + await page.setContent(htmlContent); + using elementHandle = (await page.$('#myId'))!; + const content = await elementHandle.$eval('.a', node => { + return (node as HTMLElement).innerText; + }); + expect(content).toBe('a-child-div'); + }); + + it('should throw in case of missing selector', async () => { + const {page} = await getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + using elementHandle = (await page.$('#myId'))!; + const errorMessage = await elementHandle + .$eval('.a', node => { + return (node as HTMLElement).innerText; + }) + .catch(error => { + return error.message; + }); + expect(errorMessage).toBe( + `Error: failed to find element matching selector ".a"` + ); + }); + }); + describe('ElementHandle.$$eval', function () { + it('should work', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div class="tweet"><div class="like">100</div><div class="like">10</div></div></body></html>' + ); + using tweet = (await page.$('.tweet'))!; + const content = await tweet.$$eval('.like', nodes => { + return (nodes as HTMLElement[]).map(n => { + return n.innerText; + }); + }); + expect(content).toEqual(['100', '10']); + }); + + it('should retrieve content from subtree', async () => { + const {page} = await getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"><div class="a">a1-child-div</div><div class="a">a2-child-div</div></div>'; + await page.setContent(htmlContent); + using elementHandle = (await page.$('#myId'))!; + const content = await elementHandle.$$eval('.a', nodes => { + return (nodes as HTMLElement[]).map(n => { + return n.innerText; + }); + }); + expect(content).toEqual(['a1-child-div', 'a2-child-div']); + }); + + it('should not throw in case of missing selector', async () => { + const {page} = await getTestState(); + + const htmlContent = + '<div class="a">not-a-child-div</div><div id="myId"></div>'; + await page.setContent(htmlContent); + using elementHandle = (await page.$('#myId'))!; + const nodesLength = await elementHandle.$$eval('.a', nodes => { + return nodes.length; + }); + expect(nodesLength).toBe(0); + }); + }); + + describe('ElementHandle.$$', function () { + it('should query existing elements', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + using html = (await page.$('html'))!; + const elements = await html.$$('div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate((e: HTMLElement) => { + return e.textContent; + }, element); + }); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('should return empty array for non-existing elements', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + using html = (await page.$('html'))!; + const elements = await html.$$('div'); + expect(elements).toHaveLength(0); + }); + }); + + describe('ElementHandle.$x', function () { + it('should query existing element', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.PREFIX + '/playground.html'); + await page.setContent( + '<html><body><div class="second"><div class="inner">A</div></div></body></html>' + ); + using html = (await page.$('html'))!; + const second = await html.$x(`./body/div[contains(@class, 'second')]`); + const inner = await second[0]!.$x(`./div[contains(@class, 'inner')]`); + const content = await page.evaluate(e => { + return e.textContent; + }, inner[0]!); + expect(content).toBe('A'); + }); + + it('should return null for non-existing element', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div class="second"><div class="inner">B</div></div></body></html>' + ); + using html = (await page.$('html'))!; + const second = await html.$x(`/div[contains(@class, 'third')]`); + expect(second).toEqual([]); + }); + }); + + // This is the same tests for `$$eval` and `$$` as above, but with a queryAll + // handler that returns an array instead of a list of nodes. + describe('QueryAll', function () { + const handler: CustomQueryHandler = { + queryAll: (element, selector) => { + return [...(element as Element).querySelectorAll(selector)]; + }, + }; + before(() => { + Puppeteer.registerCustomQueryHandler('allArray', handler); + }); + + it('should have registered handler', async () => { + expect( + Puppeteer.customQueryHandlerNames().includes('allArray') + ).toBeTruthy(); + }); + it('$$ should query existing elements', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><div>A</div><br/><div>B</div></body></html>' + ); + using html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements).toHaveLength(2); + const promises = elements.map(element => { + return page.evaluate(e => { + return e.textContent; + }, element); + }); + expect(await Promise.all(promises)).toEqual(['A', 'B']); + }); + + it('$$ should return empty array for non-existing elements', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<html><body><span>A</span><br/><span>B</span></body></html>' + ); + using html = (await page.$('html'))!; + const elements = await html.$$('allArray/div'); + expect(elements).toHaveLength(0); + }); + it('$$eval should work', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCount = await page.$$eval('allArray/div', divs => { + return divs.length; + }); + expect(divsCount).toBe(3); + }); + it('$$eval should accept extra arguments', async () => { + const {page} = await getTestState(); + await page.setContent( + '<div>hello</div><div>beautiful</div><div>world!</div>' + ); + const divsCountPlus5 = await page.$$eval( + 'allArray/div', + (divs, two, three) => { + return divs.length + (two as number) + (three as number); + }, + 2, + 3 + ); + expect(divsCountPlus5).toBe(8); + }); + it('$$eval should accept ElementHandles as arguments', async () => { + const {page} = await getTestState(); + await page.setContent( + '<section>2</section><section>2</section><section>1</section><div>3</div>' + ); + using divHandle = (await page.$('div'))!; + const sum = await page.$$eval( + 'allArray/section', + (sections, div) => { + return ( + sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0) + Number((div as HTMLElement).textContent) + ); + }, + divHandle + ); + expect(sum).toBe(8); + }); + it('$$eval should handle many elements', async function () { + this.timeout(25_000); + + const {page} = await getTestState(); + await page.evaluate( + ` + for (var i = 0; i <= 1000; i++) { + const section = document.createElement('section'); + section.textContent = i; + document.body.appendChild(section); + } + ` + ); + const sum = await page.$$eval('allArray/section', sections => { + return sections.reduce((acc, section) => { + return acc + Number(section.textContent); + }, 0); + }); + expect(sum).toBe(500500); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts new file mode 100644 index 0000000000..966554fd5d --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception-experimental.spec.ts @@ -0,0 +1,969 @@ +/** + * @license + * Copyright 2021 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; +import { + type ActionResult, + type HTTPRequest, + InterceptResolutionAction, +} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('cooperative request interception', function () { + setupTestBrowserHooks(); + + describe('Page.setRequestInterception', function () { + const expectedActions: ActionResult[] = ['abort', 'continue', 'respond']; + while (expectedActions.length > 0) { + const expectedAction = expectedActions.pop(); + it(`should cooperatively ${expectedAction} by priority`, async () => { + const {page, server} = await getTestState(); + + const actionResults: ActionResult[] = []; + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + void request.continue( + {headers: {...request.headers(), xaction: 'continue'}}, + expectedAction === 'continue' ? 1 : 0 + ); + } else { + void request.continue({}, 0); + } + }); + page.on('request', request => { + if (request.url().endsWith('.css')) { + void request.respond( + {headers: {xaction: 'respond'}}, + expectedAction === 'respond' ? 1 : 0 + ); + } else { + void request.continue({}, 0); + } + }); + page.on('request', request => { + if (request.url().endsWith('.css')) { + void request.abort('aborted', expectedAction === 'abort' ? 1 : 0); + } else { + void request.continue({}, 0); + } + }); + page.on('response', response => { + const {xaction} = response!.headers(); + if (response!.url().endsWith('.css') && !!xaction) { + actionResults.push(xaction as ActionResult); + } + }); + page.on('requestfailed', request => { + if (request.url().endsWith('.css')) { + actionResults.push('abort'); + } + }); + + const response = (await (async () => { + if (expectedAction === 'continue') { + const [serverRequest, response] = await Promise.all([ + server.waitForRequest('/one-style.css'), + page.goto(server.PREFIX + '/one-style.html'), + ]); + actionResults.push( + serverRequest.headers['xaction'] as ActionResult + ); + return response; + } else { + return await page.goto(server.PREFIX + '/one-style.html'); + } + })())!; + + expect(actionResults).toHaveLength(1); + expect(actionResults[0]).toBe(expectedAction); + expect(response!.ok()).toBe(true); + }); + } + + it('should intercept', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (isFavicon(request)) { + void request.continue({}, 0); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame()!.url()).toBe('about:blank'); + void request.continue({}, 0); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response!.ok()).toBe(true); + expect(response!.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + await page.setContent(` + <form action='/rredirect' method='post'> + <input type="hidden" id="foo" name="foo" value="FOOBAR"> + </form> + `); + await Promise.all([ + page.$eval('form', form => { + return (form as HTMLFormElement).submit(); + }), + page.waitForNavigation(), + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3973 + it('should work when header manipulation headers with redirect', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + void request.continue({headers}, 0); + + expect(request.continueRequestOverrides()).toEqual({headers}); + }); + // Make sure that the goto does not time out. + await page.goto(server.PREFIX + '/rrredirect'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4743 + it('should be able to remove headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + void request.continue({headers}, 0); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/empty.html'), + ]); + + expect(serverRequest.headers.origin).toBe(undefined); + }); + it('should contain referer header', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + if (!isFavicon(request)) { + requests.push(request); + } + void request.continue({}, 0); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1]!.url()).toContain('/one-style.css'); + expect(requests[1]!.headers()['referer']).toContain('/one-style.html'); + }); + it('should properly return navigation response when URL has cookies', async () => { + const {page, server} = await getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({name: 'foo', value: 'bar'}); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + const response = await page.reload(); + expect(response!.status()).toBe(200); + }); + it('should stop intercepting', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.once('request', request => { + return request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(false); + await page.goto(server.EMPTY_PAGE); + }); + it('should show custom HTTP headers', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['foo']).toBe('bar'); + void request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.ok()).toBe(true); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4337 + it('should work with redirect inside sync XHR', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRedirect('/logo.png', '/pptr.png'); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + const status = await page.evaluate(async () => { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }); + expect(status).toBe(200); + }); + it('should work with custom referer headers', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE}); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + void request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.ok()).toBe(true); + }); + it('should be abortable', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + void request.abort('failed', 0); + } else { + void request.continue({}, 0); + } + }); + let failedRequests = 0; + page.on('requestfailed', () => { + return ++failedRequests; + }); + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response!.ok()).toBe(true); + expect(response!.request().failure()).toBe(null); + expect(failedRequests).toBe(1); + }); + it('should be able to access the error reason', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.abort('failed', 0); + }); + let abortReason = null; + page.on('request', request => { + abortReason = request.abortErrorReason(); + void request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE).catch(() => {}); + expect(abortReason).toBe('Failed'); + }); + it('should be abortable with custom error codes', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.abort('internetdisconnected', 0); + }); + + const [failedRequest] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'requestfailed'), + page.goto(server.EMPTY_PAGE).catch(() => {}), + ]); + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure()!.errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + const [request] = await Promise.all([ + server.waitForRequest('/grid.html'), + page.goto(server.PREFIX + '/grid.html'), + ]); + expect(request.headers['referer']).toBe('http://google.com/'); + }); + it('should fail navigation when aborting main resource', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.abort('failed', 0); + }); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + if (isChrome) { + expect(error.message).toContain('net::ERR_FAILED'); + } else { + expect(error.message).toContain('NS_ERROR_FAILURE'); + } + }); + it('should work with redirects', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue({}, 0); + requests.push(request); + }); + server.setRedirect( + '/non-existing-page.html', + '/non-existing-page-2.html' + ); + server.setRedirect( + '/non-existing-page-2.html', + '/non-existing-page-3.html' + ); + server.setRedirect( + '/non-existing-page-3.html', + '/non-existing-page-4.html' + ); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); + const response = await page.goto( + server.PREFIX + '/non-existing-page.html' + ); + expect(response!.status()).toBe(200); + expect(response!.url()).toContain('empty.html'); + expect(requests).toHaveLength(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response!.request().redirectChain(); + expect(redirectChain).toHaveLength(4); + expect(redirectChain[0]!.url()).toContain('/non-existing-page.html'); + expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]!; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue({}, 0); + if (!isFavicon(request)) { + requests.push(request); + } + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (_req, res) => { + return res.end('body {box-sizing: border-box; }'); + }); + + const response = await page.goto(server.PREFIX + '/one-style.html'); + expect(response!.status()).toBe(200); + expect(response!.url()).toContain('one-style.html'); + expect(requests).toHaveLength(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain).toHaveLength(3); + expect(redirectChain[0]!.url()).toContain('/one-style.css'); + expect(redirectChain[2]!.url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', request => { + if (request.url().includes('non-existing-2')) { + void request.abort('failed', 0); + } else { + void request.continue({}, 0); + } + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + return await fetch('/non-existing.json'); + } catch (error) { + return (error as Error).message; + } + }); + if (isChrome) { + expect(result).toContain('Failed to fetch'); + } else { + expect(result).toContain('NetworkError'); + } + }); + it('should work with equal requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (_req, res) => { + return res.end(responseCount++ * 11 + ''); + }); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', request => { + if (isFavicon(request)) { + void request.continue({}, 0); + return; + } + void (spinner ? request.abort('failed', 0) : request.continue({}, 0)); + spinner = !spinner; + }); + const results = await page.evaluate(() => { + return Promise.all([ + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response!.text(); + }) + .catch(() => { + return 'FAILED'; + }), + ]); + }); + expect(results).toEqual(['11', 'FAILED', '22']); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const {page} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + void request.continue({}, 0); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = await page.goto(dataURL); + expect(response!.status()).toBe(200); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + void request.continue({}, 0); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('<div>yo</div>'); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + void request.continue({}, 0); + }); + const response = await page.goto(server.EMPTY_PAGE + '#hash'); + expect(response!.status()).toBe(200); + expect(response!.url()).toBe(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const {page, server} = await getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + const response = await page.goto( + server.PREFIX + '/some nonexisting page' + ); + expect(response!.status()).toBe(404); + }); + it('should work with badly encoded server', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + server.setRoute('/malformed?rnd=%911', (_req, res) => { + return res.end(); + }); + page.on('request', request => { + return request.continue({}, 0); + }); + const response = await page.goto(server.PREFIX + '/malformed?rnd=%911'); + expect(response!.status()).toBe(200); + }); + it('should work with encoded server - 2', async () => { + const {page, server} = await getTestState(); + + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue({}, 0); + requests.push(request); + }); + const response = await page.goto( + `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>` + ); + expect(response!.status()).toBe(200); + expect(requests).toHaveLength(2); + expect(requests[1]!.response()!.status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const {page, server} = await getTestState(); + + await page.setContent('<iframe></iframe>'); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + void (page.$eval( + 'iframe', + (frame, url) => { + return ((frame as HTMLIFrameElement).src = url as string); + }, + server.EMPTY_PAGE + ), + // Wait for request interception. + await waitEvent(page, 'request')); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', frame => { + return frame.remove(); + }); + let error!: Error; + await request.continue({}, 0).catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should throw if interception is not enabled', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + page.on('request', async request => { + try { + await request.continue({}, 0); + } catch (error_) { + error = error_ as Error; + } + }); + await page.goto(server.EMPTY_PAGE); + expect(error.message).toContain('Request Interception is not enabled'); + }); + it('should work with file URLs', async () => { + const {page} = await getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', request => { + urls.add(request.url().split('/').pop()); + void request.continue({}, 0); + }); + await page.goto( + pathToFileURL(path.join(__dirname, '../assets', 'one-style.html')) + ); + expect(urls.size).toBe(2); + expect(urls.has('one-style.html')).toBe(true); + expect(urls.has('one-style.css')).toBe(true); + }); + it('should not cache if cache disabled', async () => { + const {page, server} = await getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(false); + page.on('request', request => { + return request.continue({}, 0); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(0); + }); + it('should cache if cache enabled', async () => { + const {page, server} = await getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue({}, 0); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(1); + }); + it('should load fonts if cache enabled', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue({}, 0); + }); + + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await page.waitForResponse(r => { + return r.url().endsWith('/one-style.woff'); + }); + }); + }); + + describe('Request.continue', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE); + }); + it('should amend HTTP headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + void request.continue({headers}, 0); + }); + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz'); + }), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should redirect in a way non-observable to page', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + void request.continue({url: redirectURL}, 0); + }); + + const [consoleMessage] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.goto(server.EMPTY_PAGE), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({method: 'POST'}, 0); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz'); + }), + ]); + expect(request.method).toBe('POST'); + }); + it('should amend post data', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({postData: 'doggo'}, 0); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'}); + }), + ]); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should amend both post data and method on navigation', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({method: 'POST', postData: 'doggo'}, 0); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(serverRequest.method).toBe('POST'); + expect(await serverRequest.postBody).toBe('doggo'); + }); + }); + + describe('Request.respond', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond( + { + status: 201, + headers: { + foo: 'bar', + }, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(201); + expect(response!.headers()['foo']).toBe('bar'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should be able to access the response', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond( + { + status: 200, + body: 'Yo, page!', + }, + 0 + ); + }); + let response = null; + page.on('request', request => { + response = request.responseForRequest(); + void request.continue({}, 0); + }); + await page.goto(server.EMPTY_PAGE); + expect(response).toEqual({status: 200, body: 'Yo, page!'}); + }); + it('should work with status code 422', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond( + { + status: 422, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(422); + expect(response!.statusText()).toBe('Unprocessable Entity'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should redirect', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (!request.url().includes('rrredirect')) { + void request.continue({}, 0); + return; + } + void request.respond( + { + status: 302, + headers: { + location: server.EMPTY_PAGE, + }, + }, + 0 + ); + }); + const response = await page.goto(server.PREFIX + '/rrredirect'); + expect(response!.request().redirectChain()).toHaveLength(1); + expect(response!.request().redirectChain()[0]!.url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response!.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking binary responses', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + void request.respond( + { + contentType: 'image/png', + body: imageBuffer, + }, + 0 + ); + }); + await page.evaluate(PREFIX => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise(fulfill => { + return (img.onload = fulfill); + }); + }, server.PREFIX); + using img = (await page.$('img'))!; + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond( + { + status: 200, + headers: { + foo: true, + }, + body: 'Yo, page!', + }, + 0 + ); + }); + const response = await page.goto(server.EMPTY_PAGE); + expect(response!.status()).toBe(200); + const headers = response!.headers(); + expect(headers['foo']).toBe('true'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should indicate already-handled if an intercept has been handled', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue(); + }); + page.on('request', request => { + expect(request.isInterceptResolutionHandled()).toBeTruthy(); + }); + page.on('request', request => { + const {action} = request.interceptResolutionState(); + expect(action).toBe(InterceptResolutionAction.AlreadyHandled); + }); + await page.goto(server.EMPTY_PAGE); + }); + }); +}); + +function pathToFileURL(path: string): string { + let pathName = path.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash. + if (!pathName.startsWith('/')) { + pathName = '/' + pathName; + } + return 'file://' + pathName; +} diff --git a/remote/test/puppeteer/test/src/requestinterception.spec.ts b/remote/test/puppeteer/test/src/requestinterception.spec.ts new file mode 100644 index 0000000000..45827bb3cf --- /dev/null +++ b/remote/test/puppeteer/test/src/requestinterception.spec.ts @@ -0,0 +1,920 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; +import type {HTTPRequest} from 'puppeteer-core/internal/api/HTTPRequest.js'; +import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {isFavicon, waitEvent} from './utils.js'; + +describe('request interception', function () { + setupTestBrowserHooks(); + + describe('Page.setRequestInterception', function () { + it('should intercept', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (isFavicon(request)) { + void request.continue(); + return; + } + expect(request.url()).toContain('empty.html'); + expect(request.headers()['user-agent']).toBeTruthy(); + expect(request.headers()['accept']).toBeTruthy(); + expect(request.method()).toBe('GET'); + expect(request.postData()).toBe(undefined); + expect(request.isNavigationRequest()).toBe(true); + expect(request.resourceType()).toBe('document'); + expect(request.frame() === page.mainFrame()).toBe(true); + expect(request.frame()!.url()).toBe('about:blank'); + void request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + expect(response.remoteAddress().port).toBe(server.PORT); + }); + // @see https://github.com/puppeteer/puppeteer/pull/3105 + it('should work when POST is redirected with 302', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/rredirect', '/empty.html'); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + await page.setContent(` + <form action='/rredirect' method='post'> + <input type="hidden" id="foo" name="foo" value="FOOBAR"> + </form> + `); + await Promise.all([ + page.$eval('form', form => { + return (form as HTMLFormElement).submit(); + }), + page.waitForNavigation(), + ]); + }); + // @see https://github.com/puppeteer/puppeteer/issues/3973 + it('should work when header manipulation headers with redirect', async () => { + const {page, server} = await getTestState(); + + server.setRedirect('/rrredirect', '/empty.html'); + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + }); + void request.continue({headers}); + }); + await page.goto(server.PREFIX + '/rrredirect'); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4743 + it('should be able to remove headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers(), { + foo: 'bar', + origin: undefined, // remove "origin" header + }); + void request.continue({headers}); + }); + + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.PREFIX + '/empty.html'), + ]); + + expect(serverRequest.headers.origin).toBe(undefined); + }); + it('should contain referer header', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + if (!isFavicon(request)) { + requests.push(request); + } + void request.continue(); + }); + await page.goto(server.PREFIX + '/one-style.html'); + expect(requests[1]!.url()).toContain('/one-style.css'); + expect(requests[1]!.headers()['referer']).toContain('/one-style.html'); + }); + it('should work with requests without networkId', async () => { + const {page, server} = await getTestState(); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + + const cdp = await page.target().createCDPSession(); + await cdp.send('DOM.enable'); + const urls: string[] = []; + page.on('request', request => { + urls.push(request.url()); + return request.continue(); + }); + // This causes network requests without networkId. + await cdp.send('CSS.enable'); + expect(urls).toStrictEqual([server.EMPTY_PAGE]); + }); + it('should properly return navigation response when URL has cookies', async () => { + const {page, server} = await getTestState(); + + // Setup cookie. + await page.goto(server.EMPTY_PAGE); + await page.setCookie({name: 'foo', value: 'bar'}); + + // Setup request interception. + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const response = (await page.reload())!; + expect(response.status()).toBe(200); + }); + it('should stop intercepting', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.once('request', request => { + return request.continue(); + }); + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(false); + await page.goto(server.EMPTY_PAGE); + }); + it('should show custom HTTP headers', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({ + foo: 'bar', + }); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['foo']).toBe('bar'); + void request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + }); + // @see https://github.com/puppeteer/puppeteer/issues/4337 + it('should work with redirect inside sync XHR', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + server.setRedirect('/logo.png', '/pptr.png'); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const status = await page.evaluate(async () => { + const request = new XMLHttpRequest(); + request.open('GET', '/logo.png', false); // `false` makes the request synchronous + request.send(null); + return request.status; + }); + expect(status).toBe(200); + }); + it('should work with custom referer headers', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({referer: server.EMPTY_PAGE}); + await page.setRequestInterception(true); + page.on('request', request => { + expect(request.headers()['referer']).toBe(server.EMPTY_PAGE); + void request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.ok()).toBe(true); + }); + it('should be abortable', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (request.url().endsWith('.css')) { + void request.abort(); + } else { + void request.continue(); + } + }); + let failedRequests = 0; + page.on('requestfailed', () => { + return ++failedRequests; + }); + const response = (await page.goto(server.PREFIX + '/one-style.html'))!; + expect(response.ok()).toBe(true); + expect(response.request().failure()).toBe(null); + expect(failedRequests).toBe(1); + }); + it('should be abortable with custom error codes', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.abort('internetdisconnected'); + }); + const [failedRequest] = await Promise.all([ + waitEvent<HTTPRequest>(page, 'requestfailed'), + page.goto(server.EMPTY_PAGE).catch(() => {}), + ]); + + expect(failedRequest).toBeTruthy(); + expect(failedRequest.failure()!.errorText).toBe( + 'net::ERR_INTERNET_DISCONNECTED' + ); + }); + it('should send referer', async () => { + const {page, server} = await getTestState(); + + await page.setExtraHTTPHeaders({ + referer: 'http://google.com/', + }); + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const [request] = await Promise.all([ + server.waitForRequest('/grid.html'), + page.goto(server.PREFIX + '/grid.html'), + ]); + expect(request.headers['referer']).toBe('http://google.com/'); + }); + it('should fail navigation when aborting main resource', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.abort(); + }); + let error!: Error; + await page.goto(server.EMPTY_PAGE).catch(error_ => { + return (error = error_); + }); + expect(error).toBeTruthy(); + if (isChrome) { + expect(error.message).toContain('net::ERR_FAILED'); + } else { + expect(error.message).toContain('NS_ERROR_FAILURE'); + } + }); + it('should work with redirects', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue(); + requests.push(request); + }); + server.setRedirect( + '/non-existing-page.html', + '/non-existing-page-2.html' + ); + server.setRedirect( + '/non-existing-page-2.html', + '/non-existing-page-3.html' + ); + server.setRedirect( + '/non-existing-page-3.html', + '/non-existing-page-4.html' + ); + server.setRedirect('/non-existing-page-4.html', '/empty.html'); + const response = (await page.goto( + server.PREFIX + '/non-existing-page.html' + ))!; + expect(response.status()).toBe(200); + expect(response.url()).toContain('empty.html'); + expect(requests).toHaveLength(5); + expect(requests[2]!.resourceType()).toBe('document'); + // Check redirect chain + const redirectChain = response.request().redirectChain(); + expect(redirectChain).toHaveLength(4); + expect(redirectChain[0]!.url()).toContain('/non-existing-page.html'); + expect(redirectChain[2]!.url()).toContain('/non-existing-page-3.html'); + for (let i = 0; i < redirectChain.length; ++i) { + const request = redirectChain[i]!; + expect(request.isNavigationRequest()).toBe(true); + expect(request.redirectChain().indexOf(request)).toBe(i); + } + }); + it('should work with redirects for subresources', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue(); + if (!isFavicon(request)) { + requests.push(request); + } + }); + server.setRedirect('/one-style.css', '/two-style.css'); + server.setRedirect('/two-style.css', '/three-style.css'); + server.setRedirect('/three-style.css', '/four-style.css'); + server.setRoute('/four-style.css', (_req, res) => { + return res.end('body {box-sizing: border-box; }'); + }); + + const response = (await page.goto(server.PREFIX + '/one-style.html'))!; + expect(response.status()).toBe(200); + expect(response.url()).toContain('one-style.html'); + expect(requests).toHaveLength(5); + expect(requests[0]!.resourceType()).toBe('document'); + expect(requests[1]!.resourceType()).toBe('stylesheet'); + // Check redirect chain + const redirectChain = requests[1]!.redirectChain(); + expect(redirectChain).toHaveLength(3); + expect(redirectChain[0]!.url()).toContain('/one-style.css'); + expect(redirectChain[2]!.url()).toContain('/three-style.css'); + }); + it('should be able to abort redirects', async () => { + const {page, server, isChrome} = await getTestState(); + + await page.setRequestInterception(true); + server.setRedirect('/non-existing.json', '/non-existing-2.json'); + server.setRedirect('/non-existing-2.json', '/simple.html'); + page.on('request', request => { + if (request.url().includes('non-existing-2')) { + void request.abort(); + } else { + void request.continue(); + } + }); + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(async () => { + try { + return await fetch('/non-existing.json'); + } catch (error) { + return (error as Error).message; + } + }); + if (isChrome) { + expect(result).toContain('Failed to fetch'); + } else { + expect(result).toContain('NetworkError'); + } + }); + it('should work with equal requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let responseCount = 1; + server.setRoute('/zzz', (_req, res) => { + return res.end(responseCount++ * 11 + ''); + }); + await page.setRequestInterception(true); + + let spinner = false; + // Cancel 2nd request. + page.on('request', request => { + if (isFavicon(request)) { + void request.continue(); + return; + } + void (spinner ? request.abort() : request.continue()); + spinner = !spinner; + }); + const results = await page.evaluate(() => { + return Promise.all([ + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return 'FAILED'; + }), + fetch('/zzz') + .then(response => { + return response.text(); + }) + .catch(() => { + return 'FAILED'; + }), + ]); + }); + expect(results).toEqual(['11', 'FAILED', '22']); + }); + it('should navigate to dataURL and fire dataURL requests', async () => { + const {page} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + void request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const response = (await page.goto(dataURL))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should be able to fetch dataURL and fire dataURL requests', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + !isFavicon(request) && requests.push(request); + void request.continue(); + }); + const dataURL = 'data:text/html,<div>yo</div>'; + const text = await page.evaluate((url: string) => { + return fetch(url).then(r => { + return r.text(); + }); + }, dataURL); + expect(text).toBe('<div>yo</div>'); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(dataURL); + }); + it('should navigate to URL with hash and fire requests without hash', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + requests.push(request); + void request.continue(); + }); + const response = (await page.goto(server.EMPTY_PAGE + '#hash'))!; + expect(response.status()).toBe(200); + expect(response.url()).toBe(server.EMPTY_PAGE); + expect(requests).toHaveLength(1); + expect(requests[0]!.url()).toBe(server.EMPTY_PAGE); + }); + it('should work with encoded server', async () => { + const {page, server} = await getTestState(); + + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + const response = (await page.goto( + server.PREFIX + '/some nonexisting page' + ))!; + expect(response.status()).toBe(404); + }); + it('should work with badly encoded server', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + server.setRoute('/malformed?rnd=%911', (_req, res) => { + return res.end(); + }); + page.on('request', request => { + return request.continue(); + }); + const response = (await page.goto( + server.PREFIX + '/malformed?rnd=%911' + ))!; + expect(response.status()).toBe(200); + }); + it('should work wit h encoded server - 2', async () => { + const {page, server} = await getTestState(); + + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + await page.setRequestInterception(true); + const requests: HTTPRequest[] = []; + page.on('request', request => { + void request.continue(); + requests.push(request); + }); + const response = (await page.goto( + `data:text/html,<link rel="stylesheet" href="${server.PREFIX}/fonts?helvetica|arial"/>` + ))!; + expect(response.status()).toBe(200); + expect(requests).toHaveLength(2); + expect(requests[1]!.response()!.status()).toBe(404); + }); + it('should not throw "Invalid Interception Id" if the request was cancelled', async () => { + const {page, server} = await getTestState(); + + await page.setContent('<iframe></iframe>'); + await page.setRequestInterception(true); + let request!: HTTPRequest; + page.on('request', async r => { + return (request = r); + }); + void (page.$eval( + 'iframe', + (frame, url) => { + return ((frame as HTMLIFrameElement).src = url as string); + }, + server.EMPTY_PAGE + ), + // Wait for request interception. + await waitEvent(page, 'request')); + // Delete frame to cause request to be canceled. + await page.$eval('iframe', frame => { + return frame.remove(); + }); + let error!: Error; + await request.continue().catch(error_ => { + return (error = error_); + }); + expect(error).toBeUndefined(); + }); + it('should throw if interception is not enabled', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + page.on('request', async request => { + try { + await request.continue(); + } catch (error_) { + error = error_ as Error; + } + }); + await page.goto(server.EMPTY_PAGE); + expect(error.message).toContain('Request Interception is not enabled'); + }); + it('should work with file URLs', async () => { + const {page} = await getTestState(); + + await page.setRequestInterception(true); + const urls = new Set(); + page.on('request', request => { + urls.add(request.url().split('/').pop()); + void request.continue(); + }); + await page.goto( + pathToFileURL(path.join(__dirname, '../assets', 'one-style.html')) + ); + expect(urls.size).toBe(2); + expect(urls.has('one-style.html')).toBe(true); + expect(urls.has('one-style.css')).toBe(true); + }); + it('should not cache if cache disabled', async () => { + const {page, server} = await getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(false); + page.on('request', request => { + return request.continue(); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(0); + }); + it('should cache if cache enabled', async () => { + const {page, server} = await getTestState(); + + // Load and re-load to make sure it's cached. + await page.goto(server.PREFIX + '/cached/one-style.html'); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue(); + }); + + const cached: HTTPRequest[] = []; + page.on('requestservedfromcache', r => { + return cached.push(r); + }); + + await page.reload(); + expect(cached).toHaveLength(1); + }); + it('should load fonts if cache enabled', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + await page.setCacheEnabled(true); + page.on('request', request => { + return request.continue(); + }); + + const responsePromise = page.waitForResponse(r => { + return r.url().endsWith('/one-style.woff'); + }); + await page.goto(server.PREFIX + '/cached/one-style-font.html'); + await responsePromise; + }); + }); + + describe('Request.continue', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + return request.continue(); + }); + await page.goto(server.EMPTY_PAGE); + }); + it('should amend HTTP headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const headers = Object.assign({}, request.headers()); + headers['FOO'] = 'bar'; + void request.continue({headers}); + }); + await page.goto(server.EMPTY_PAGE); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz'); + }), + ]); + expect(request.headers['foo']).toBe('bar'); + }); + it('should redirect in a way non-observable to page', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const redirectURL = request.url().includes('/empty.html') + ? server.PREFIX + '/consolelog.html' + : undefined; + void request.continue({url: redirectURL}); + }); + const [consoleMessage] = await Promise.all([ + waitEvent<ConsoleMessage>(page, 'console'), + page.goto(server.EMPTY_PAGE), + ]); + expect(page.url()).toBe(server.EMPTY_PAGE); + expect(consoleMessage.text()).toBe('yellow'); + }); + it('should amend method', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({method: 'POST'}); + }); + const [request] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz'); + }), + ]); + expect(request.method).toBe('POST'); + }); + it('should amend post data', async () => { + const {page, server} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({postData: 'doggo'}); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/sleep.zzz'), + page.evaluate(() => { + return fetch('/sleep.zzz', {method: 'POST', body: 'birdy'}); + }), + ]); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should amend both post data and method on navigation', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.continue({method: 'POST', postData: 'doggo'}); + }); + const [serverRequest] = await Promise.all([ + server.waitForRequest('/empty.html'), + page.goto(server.EMPTY_PAGE), + ]); + expect(serverRequest.method).toBe('POST'); + expect(await serverRequest.postBody).toBe('doggo'); + }); + it('should fail if the header value is invalid', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + await page.setRequestInterception(true); + page.on('request', async request => { + await request + .continue({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch(error_ => { + error = error_ as Error; + }); + await request.continue(); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); + + describe('Request.respond', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond({ + status: 201, + headers: { + foo: 'bar', + }, + body: 'Yo, page!', + }); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(201); + expect(response.headers()['foo']).toBe('bar'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should work with status code 422', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond({ + status: 422, + body: 'Yo, page!', + }); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(422); + expect(response.statusText()).toBe('Unprocessable Entity'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should redirect', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + if (!request.url().includes('rrredirect')) { + void request.continue(); + return; + } + void request.respond({ + status: 302, + headers: { + location: server.EMPTY_PAGE, + }, + }); + }); + const response = (await page.goto(server.PREFIX + '/rrredirect'))!; + expect(response.request().redirectChain()).toHaveLength(1); + expect(response.request().redirectChain()[0]!.url()).toBe( + server.PREFIX + '/rrredirect' + ); + expect(response.url()).toBe(server.EMPTY_PAGE); + }); + it('should allow mocking multiple headers with same key', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond({ + status: 200, + headers: { + foo: 'bar', + arr: ['1', '2'], + 'set-cookie': ['first=1', 'second=2'], + }, + body: 'Hello world', + }); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + const cookies = await page.cookies(); + const firstCookie = cookies.find(cookie => { + return cookie.name === 'first'; + }); + const secondCookie = cookies.find(cookie => { + return cookie.name === 'second'; + }); + expect(response.status()).toBe(200); + expect(response.headers()['foo']).toBe('bar'); + expect(response.headers()['arr']).toBe('1\n2'); + // request.respond() will not trigger Network.responseReceivedExtraInfo + // fail to get 'set-cookie' header from response + expect(firstCookie?.value).toBe('1'); + expect(secondCookie?.value).toBe('2'); + }); + it('should allow mocking binary responses', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + const imageBuffer = fs.readFileSync( + path.join(__dirname, '../assets', 'pptr.png') + ); + void request.respond({ + contentType: 'image/png', + body: imageBuffer, + }); + }); + await page.evaluate(PREFIX => { + const img = document.createElement('img'); + img.src = PREFIX + '/does-not-exist.png'; + document.body.appendChild(img); + return new Promise(fulfill => { + return (img.onload = fulfill); + }); + }, server.PREFIX); + using img = (await page.$('img'))!; + expect(await img.screenshot()).toBeGolden('mock-binary-response.png'); + }); + it('should stringify intercepted request response headers', async () => { + const {page, server} = await getTestState(); + + await page.setRequestInterception(true); + page.on('request', request => { + void request.respond({ + status: 200, + headers: { + foo: true, + }, + body: 'Yo, page!', + }); + }); + const response = (await page.goto(server.EMPTY_PAGE))!; + expect(response.status()).toBe(200); + const headers = response.headers(); + expect(headers['foo']).toBe('true'); + expect( + await page.evaluate(() => { + return document.body.textContent; + }) + ).toBe('Yo, page!'); + }); + it('should fail if the header value is invalid', async () => { + const {page, server} = await getTestState(); + + let error!: Error; + await page.setRequestInterception(true); + page.on('request', async request => { + await request + .respond({ + headers: { + 'X-Invalid-Header': 'a\nb', + }, + }) + .catch(error_ => { + error = error_ as Error; + }); + await request.respond({ + status: 200, + body: 'Hello World', + }); + }); + await page.goto(server.PREFIX + '/empty.html'); + expect(error.message).toMatch(/Invalid header/); + }); + }); +}); + +function pathToFileURL(path: string): string { + let pathName = path.replace(/\\/g, '/'); + // Windows drive letter must be prefixed with a slash. + if (!pathName.startsWith('/')) { + pathName = '/' + pathName; + } + return 'file://' + pathName; +} diff --git a/remote/test/puppeteer/test/src/screencast.spec.ts b/remote/test/puppeteer/test/src/screencast.spec.ts new file mode 100644 index 0000000000..b645f55da7 --- /dev/null +++ b/remote/test/puppeteer/test/src/screencast.spec.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {statSync} from 'fs'; + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {getUniqueVideoFilePlaceholder} from './utils.js'; + +describe('Screencasts', function () { + setupTestBrowserHooks(); + + describe('Page.screencast', function () { + it('should work', async () => { + using file = getUniqueVideoFilePlaceholder(); + + const {page} = await getTestState(); + + const recorder = await page.screencast({ + path: file.filename, + scale: 0.5, + crop: {width: 100, height: 100, x: 0, y: 0}, + speed: 0.5, + }); + + await page.goto('data:text/html,<input>'); + using input = await page.locator('input').waitHandle(); + await input.type('ab', {delay: 100}); + + await recorder.stop(); + + expect(statSync(file.filename).size).toBeGreaterThan(0); + }); + it('should work concurrently', async () => { + using file1 = getUniqueVideoFilePlaceholder(); + using file2 = getUniqueVideoFilePlaceholder(); + + const {page} = await getTestState(); + + const recorder = await page.screencast({path: file1.filename}); + const recorder2 = await page.screencast({path: file2.filename}); + + await page.goto('data:text/html,<input>'); + using input = await page.locator('input').waitHandle(); + + await input.type('ab', {delay: 100}); + await recorder.stop(); + + await input.type('ab', {delay: 100}); + await recorder2.stop(); + + // Since file2 spent about double the time of file1 recording, so file2 + // should be around double the size of file1. + const ratio = + statSync(file2.filename).size / statSync(file1.filename).size; + + // We use a range because we cannot be precise. + const DELTA = 1.3; + expect(ratio).toBeGreaterThan(2 - DELTA); + expect(ratio).toBeLessThan(2 + DELTA); + }); + it('should validate options', async () => { + const {page} = await getTestState(); + + await expect(page.screencast({scale: 0})).rejects.toBeDefined(); + await expect(page.screencast({scale: -1})).rejects.toBeDefined(); + + await expect(page.screencast({speed: 0})).rejects.toBeDefined(); + await expect(page.screencast({speed: -1})).rejects.toBeDefined(); + + await expect( + page.screencast({crop: {x: 0, y: 0, height: 1, width: 0}}) + ).rejects.toBeDefined(); + await expect( + page.screencast({crop: {x: 0, y: 0, height: 0, width: 1}}) + ).rejects.toBeDefined(); + await expect( + page.screencast({crop: {x: -1, y: 0, height: 1, width: 1}}) + ).rejects.toBeDefined(); + await expect( + page.screencast({crop: {x: 0, y: -1, height: 1, width: 1}}) + ).rejects.toBeDefined(); + await expect( + page.screencast({crop: {x: 0, y: 0, height: 10000, width: 1}}) + ).rejects.toBeDefined(); + await expect( + page.screencast({crop: {x: 0, y: 0, height: 1, width: 10000}}) + ).rejects.toBeDefined(); + + await expect( + page.screencast({ffmpegPath: 'non-existent-path'}) + ).rejects.toBeDefined(); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/screenshot.spec.ts b/remote/test/puppeteer/test/src/screenshot.spec.ts new file mode 100644 index 0000000000..ad53b60e95 --- /dev/null +++ b/remote/test/puppeteer/test/src/screenshot.spec.ts @@ -0,0 +1,453 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; + +import expect from 'expect'; + +import { + getTestState, + isHeadless, + launch, + setupTestBrowserHooks, +} from './mocha-utils.js'; + +describe('Screenshots', function () { + setupTestBrowserHooks(); + + describe('Page.screenshot', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot(); + expect(screenshot).toBeGolden('screenshot-sanity.png'); + }); + it('should clip rect', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 50, + y: 100, + width: 150, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-rect.png'); + }); + it('should get screenshot bigger than the viewport', async () => { + const {page, server} = await getTestState(); + await page.setViewport({width: 50, height: 50}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 25, + y: 25, + width: 100, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-offscreen-clip.png'); + }); + it('should clip clip bigger than the viewport without "captureBeyondViewport"', async () => { + const {page, server} = await getTestState(); + await page.setViewport({width: 50, height: 50}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + captureBeyondViewport: false, + clip: { + x: 25, + y: 25, + width: 100, + height: 100, + }, + }); + expect(screenshot).toBeGolden('screenshot-offscreen-clip-2.png'); + }); + it('should run in parallel', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const promises = []; + for (let i = 0; i < 3; ++i) { + promises.push( + page.screenshot({ + clip: { + x: 50 * i, + y: 0, + width: 50, + height: 50, + }, + }) + ); + } + const screenshots = await Promise.all(promises); + expect(screenshots[1]).toBeGolden('grid-cell-1.png'); + }); + it('should take fullPage screenshots', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + }); + expect(screenshot).toBeGolden('screenshot-grid-fullpage.png'); + }); + it('should take fullPage screenshots without captureBeyondViewport', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fullPage: true, + captureBeyondViewport: false, + }); + expect(screenshot).toBeGolden('screenshot-grid-fullpage-2.png'); + expect(page.viewport()).toMatchObject({width: 500, height: 500}); + }); + it('should run in parallel in multiple pages', async () => { + const {server, context} = await getTestState(); + + const N = 2; + const pages = await Promise.all( + Array(N) + .fill(0) + .map(async () => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + return page; + }) + ); + const promises = []; + for (let i = 0; i < N; ++i) { + promises.push( + pages[i]!.screenshot({ + clip: {x: 50 * i, y: 0, width: 50, height: 50}, + }) + ); + } + const screenshots = await Promise.all(promises); + for (let i = 0; i < N; ++i) { + expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); + } + await Promise.all( + pages.map(page => { + return page.close(); + }) + ); + }); + it('should work with odd clip size on Retina displays', async () => { + const {page} = await getTestState(); + + // Make sure documentElement height is at least 11px. + await page.setContent(`<div style="width: 11px; height: 11px;">`); + + const screenshot = await page.screenshot({ + clip: { + x: 0, + y: 0, + width: 11, + height: 11, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-odd-size.png'); + }); + it('should return base64', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + encoding: 'base64', + }); + expect(Buffer.from(screenshot, 'base64')).toBeGolden( + 'screenshot-sanity.png' + ); + }); + }); + + describe('ElementHandle.screenshot', function () { + it('should work', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => { + return window.scrollBy(50, 100); + }); + using elementHandle = (await page.$('.box:nth-of-type(3)'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-bounding-box.png'); + }); + it('should work with a null viewport', async () => { + const {server} = await getTestState({ + skipLaunch: true, + }); + const {browser, close} = await launch({ + defaultViewport: null, + }); + + try { + const page = await browser.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + await page.evaluate(() => { + return window.scrollBy(50, 100); + }); + using elementHandle = await page.$('.box:nth-of-type(3)'); + assert(elementHandle); + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeTruthy(); + } finally { + await close(); + } + }); + it('should take into account padding and border', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + something above + <style>div { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div></div> + `); + using elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-padding-border.png'); + }); + it('should capture full element when larger than viewport', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + + await page.setContent(` + something above + <style> + :root { + scrollbar-width: none; + } + div.to-screenshot { + border: 1px solid blue; + width: 600px; + height: 600px; + margin-left: 50px; + } + </style> + <div class="to-screenshot"></div> + `); + using elementHandle = (await page.$('div.to-screenshot'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-larger-than-viewport.png' + ); + + expect( + await page.evaluate(() => { + return { + w: window.innerWidth, + h: window.innerHeight, + }; + }) + ).toEqual({w: 500, h: 500}); + }); + it('should scroll element into view', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(` + something above + <style>div.above { + border: 2px solid blue; + background: red; + height: 1500px; + } + div.to-screenshot { + border: 2px solid blue; + background: green; + width: 50px; + height: 50px; + } + </style> + <div class="above"></div> + <div class="to-screenshot"></div> + `); + using elementHandle = (await page.$('div.to-screenshot'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden( + 'screenshot-element-scrolled-into-view.png' + ); + }); + it('should work with a rotated element', async () => { + const {page} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.setContent(`<div style="position:absolute; + top: 100px; + left: 100px; + width: 100px; + height: 100px; + background: green; + transform: rotateZ(200deg);"> </div>`); + using elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-rotate.png'); + }); + it('should fail to screenshot a detached element', async () => { + const {page} = await getTestState(); + + await page.setContent('<h1>remove this</h1>'); + using elementHandle = (await page.$('h1'))!; + await page.evaluate((element: HTMLElement) => { + return element.remove(); + }, elementHandle); + const screenshotError = await elementHandle.screenshot().catch(error => { + return error; + }); + expect(screenshotError.message).toBe( + 'Node is either not visible or not an HTMLElement' + ); + }); + it('should not hang with zero width/height element', async () => { + const {page} = await getTestState(); + + await page.setContent('<div style="width: 50px; height: 0"></div>'); + using div = (await page.$('div'))!; + const error = await div.screenshot().catch(error_ => { + return error_; + }); + expect(error.message).toBe('Node has 0 height.'); + }); + it('should work for an element with fractional dimensions', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div style="width:48.51px;height:19.8px;border:1px solid black;"></div>' + ); + using elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional.png'); + }); + it('should work for an element with an offset', async () => { + const {page} = await getTestState(); + + await page.setContent( + '<div style="position:absolute; top: 10.3px; left: 20.4px;width:50.3px;height:20.2px;border:1px solid black;"></div>' + ); + using elementHandle = (await page.$('div'))!; + const screenshot = await elementHandle.screenshot(); + expect(screenshot).toBeGolden('screenshot-element-fractional-offset.png'); + }); + it('should work with webp', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + type: 'webp', + }); + + expect(screenshot).toBeInstanceOf(Buffer); + }); + + it('should run in parallel in multiple pages', async () => { + const {browser, server} = await getTestState(); + + const context = await browser.createIncognitoBrowserContext(); + + const N = 2; + const pages = await Promise.all( + Array(N) + .fill(0) + .map(async () => { + const page = await context.newPage(); + await page.goto(server.PREFIX + '/grid.html'); + return page; + }) + ); + const promises = []; + for (let i = 0; i < N; ++i) { + promises.push( + pages[i]!.screenshot({ + clip: {x: 50 * i, y: 0, width: 50, height: 50}, + }) + ); + } + const screenshots = await Promise.all(promises); + for (let i = 0; i < N; ++i) { + expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); + } + await Promise.all( + pages.map(page => { + return page.close(); + }) + ); + + await context.close(); + }); + }); + + describe('Cdp', () => { + it('should use scale for clip', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + clip: { + x: 50, + y: 100, + width: 150, + height: 100, + scale: 2, + }, + }); + expect(screenshot).toBeGolden('screenshot-clip-rect-scale2.png'); + }); + it('should allow transparency', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({omitBackground: true}); + expect(screenshot).toBeGolden('transparent.png'); + }); + it('should render white background on jpeg file', async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 100, height: 100}); + await page.goto(server.EMPTY_PAGE); + const screenshot = await page.screenshot({ + omitBackground: true, + type: 'jpeg', + }); + expect(screenshot).toBeGolden('white.jpg'); + }); + (!isHeadless ? it : it.skip)( + 'should work in "fromSurface: false" mode', + async () => { + const {page, server} = await getTestState(); + + await page.setViewport({width: 500, height: 500}); + await page.goto(server.PREFIX + '/grid.html'); + const screenshot = await page.screenshot({ + fromSurface: false, + }); + expect(screenshot).toBeDefined(); // toBeGolden('screenshot-fromsurface-false.png'); + } + ); + }); +}); diff --git a/remote/test/puppeteer/test/src/stacktrace.spec.ts b/remote/test/puppeteer/test/src/stacktrace.spec.ts new file mode 100644 index 0000000000..b36ee56661 --- /dev/null +++ b/remote/test/puppeteer/test/src/stacktrace.spec.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'assert'; + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +const FILENAME = __filename.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); +const parseStackTrace = (stack: string): string => { + stack = stack.replace(new RegExp(FILENAME, 'g'), '<filename>'); + stack = stack.replace(/<filename>:(\d+):(\d+)/g, '<filename>:<line>:<col>'); + stack = stack.replace(/<anonymous>:(\d+):(\d+)/g, '<anonymous>:<line>:<col>'); + return stack; +}; + +describe('Stack trace', function () { + setupTestBrowserHooks(); + + it('should work', async () => { + const {page} = await getTestState(); + + const error = (await page + .evaluate(() => { + throw new Error('Test'); + }) + .catch((error: Error) => { + return error; + })) as Error; + + expect(error.name).toEqual('Error'); + expect(error.message).toEqual('Test'); + assert(error.stack); + error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>'); + expect( + parseStackTrace(error.stack).split('\n at ').slice(0, 2) + ).toMatchObject({ + ...[ + 'Error: Test', + 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + ], + }); + }); + + it('should work with handles', async () => { + const {page} = await getTestState(); + + const error = (await page + .evaluateHandle(() => { + throw new Error('Test'); + }) + .catch((error: Error) => { + return error; + })) as Error; + + expect(error.name).toEqual('Error'); + expect(error.message).toEqual('Test'); + assert(error.stack); + expect( + parseStackTrace(error.stack).split('\n at ').slice(0, 2) + ).toMatchObject({ + ...[ + 'Error: Test', + 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + ], + }); + }); + + it('should work with contiguous evaluation', async () => { + const {page} = await getTestState(); + + using thrower = await page.evaluateHandle(() => { + return () => { + throw new Error('Test'); + }; + }); + const error = (await thrower + .evaluate(thrower => { + thrower(); + }) + .catch((error: Error) => { + return error; + })) as Error; + + expect(error.name).toEqual('Error'); + expect(error.message).toEqual('Test'); + assert(error.stack); + expect( + parseStackTrace(error.stack).split('\n at ').slice(0, 3) + ).toMatchObject({ + ...[ + 'Error: Test', + 'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + ], + }); + }); + + it('should work with nested function calls', async () => { + const {page} = await getTestState(); + + const error = (await page + .evaluate(() => { + function a() { + throw new Error('Test'); + } + function b() { + a(); + } + function c() { + b(); + } + function d() { + c(); + } + d(); + }) + .catch((error: Error) => { + return error; + })) as Error; + + expect(error.name).toEqual('Error'); + expect(error.message).toEqual('Test'); + assert(error.stack); + expect( + parseStackTrace(error.stack).split('\n at ').slice(0, 6) + ).toMatchObject({ + ...[ + 'Error: Test', + 'a (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + 'b (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + 'c (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + 'd (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + 'evaluate (evaluate at Context.<anonymous> (<filename>:<line>:<col>), <anonymous>:<line>:<col>)', + ], + }); + }); + + it('should work for none error objects', async () => { + const {page} = await getTestState(); + + const [error] = await Promise.all([ + waitEvent<Error>(page, 'pageerror'), + page.evaluate(() => { + // This can happen when a 404 with HTML is returned + void Promise.reject(new Response()); + }), + ]); + + expect(error).toBeTruthy(); + }); +}); diff --git a/remote/test/puppeteer/test/src/target.spec.ts b/remote/test/puppeteer/test/src/target.spec.ts new file mode 100644 index 0000000000..28d17a4030 --- /dev/null +++ b/remote/test/puppeteer/test/src/target.spec.ts @@ -0,0 +1,343 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import type {ServerResponse} from 'http'; + +import expect from 'expect'; +import {type Target, TimeoutError} from 'puppeteer'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Target', function () { + setupTestBrowserHooks(); + + it('Browser.targets should return all of the targets', async () => { + const {browser} = await getTestState(); + + // The pages will be the testing page and the original newtab page + const targets = browser.targets(); + expect( + targets.some(target => { + return target.type() === 'page' && target.url() === 'about:blank'; + }) + ).toBeTruthy(); + expect( + targets.some(target => { + return target.type() === 'browser'; + }) + ).toBeTruthy(); + }); + it('Browser.pages should return all of the pages', async () => { + const {page, context} = await getTestState(); + + // The pages will be the testing page + const allPages = await context.pages(); + expect(allPages).toHaveLength(1); + expect(allPages).toContain(page); + }); + it('should contain browser target', async () => { + const {browser} = await getTestState(); + + const targets = browser.targets(); + const browserTarget = targets.find(target => { + return target.type() === 'browser'; + }); + expect(browserTarget).toBeTruthy(); + }); + it('should be able to use the default page in the browser', async () => { + const {page, browser} = await getTestState(); + + // The pages will be the testing page and the original newtab page + const allPages = await browser.pages(); + const originalPage = allPages.find(p => { + return p !== page; + })!; + expect( + await originalPage.evaluate(() => { + return ['Hello', 'world'].join(' '); + }) + ).toBe('Hello world'); + expect(await originalPage.$('body')).toBeTruthy(); + }); + it('should be able to use async waitForTarget', async () => { + const {page, server, context} = await getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget( + target => { + return target.page().then(page => { + return ( + page!.url() === server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + }); + }, + {timeout: 3000} + ) + .then(target => { + return target.page(); + }), + page.evaluate((url: string) => { + return window.open(url); + }, server.CROSS_PROCESS_PREFIX + '/empty.html'), + ]); + expect(otherPage!.url()).toEqual( + server.CROSS_PROCESS_PREFIX + '/empty.html' + ); + expect(page).not.toBe(otherPage); + }); + it('should report when a new page is created and closed', async () => { + const {page, server, context} = await getTestState(); + + const [otherPage] = await Promise.all([ + context + .waitForTarget( + target => { + return target.url() === server.CROSS_PROCESS_PREFIX + '/empty.html'; + }, + {timeout: 3000} + ) + .then(target => { + return target.page(); + }), + page.evaluate((url: string) => { + return window.open(url); + }, server.CROSS_PROCESS_PREFIX + '/empty.html'), + ]); + expect(otherPage!.url()).toContain(server.CROSS_PROCESS_PREFIX); + expect( + await otherPage!.evaluate(() => { + return ['Hello', 'world'].join(' '); + }) + ).toBe('Hello world'); + expect(await otherPage!.$('body')).toBeTruthy(); + + let allPages = await context.pages(); + expect(allPages).toContain(page); + expect(allPages).toContain(otherPage); + + const [closedTarget] = await Promise.all([ + waitEvent<Target>(context, 'targetdestroyed'), + otherPage!.close(), + ]); + expect(await closedTarget.page()).toBe(otherPage); + + allPages = (await Promise.all( + context.targets().map(target => { + return target.page(); + }) + )) as Page[]; + expect(allPages).toContain(page); + expect(allPages).not.toContain(otherPage); + }); + it('should report when a service worker is created and destroyed', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const createdTarget = waitEvent(context, 'targetcreated'); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + expect((await createdTarget).type()).toBe('service_worker'); + expect((await createdTarget).url()).toBe( + server.PREFIX + '/serviceworkers/empty/sw.js' + ); + + const destroyedTarget = waitEvent(context, 'targetdestroyed'); + await page.evaluate(() => { + return ( + globalThis as unknown as { + registrationPromise: Promise<{unregister: () => void}>; + } + ).registrationPromise.then((registration: any) => { + return registration.unregister(); + }); + }); + expect(await destroyedTarget).toBe(await createdTarget); + }); + it('should create a worker from a service worker', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'); + + const target = await context.waitForTarget( + target => { + return target.type() === 'service_worker'; + }, + {timeout: 3000} + ); + const worker = (await target.worker())!; + + expect( + await worker.evaluate(() => { + return self.toString(); + }) + ).toBe('[object ServiceWorkerGlobalScope]'); + }); + it('should create a worker from a shared worker', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => { + new SharedWorker('data:text/javascript,console.log("hi")'); + }); + const target = await context.waitForTarget( + target => { + return target.type() === 'shared_worker'; + }, + {timeout: 3000} + ); + const worker = (await target.worker())!; + expect( + await worker.evaluate(() => { + return self.toString(); + }) + ).toBe('[object SharedWorkerGlobalScope]'); + }); + it('should report when a target url changes', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + let changedTarget = waitEvent(context, 'targetchanged'); + await page.goto(server.CROSS_PROCESS_PREFIX + '/'); + expect((await changedTarget).url()).toBe(server.CROSS_PROCESS_PREFIX + '/'); + + changedTarget = waitEvent(context, 'targetchanged'); + await page.goto(server.EMPTY_PAGE); + expect((await changedTarget).url()).toBe(server.EMPTY_PAGE); + }); + it('should not report uninitialized pages', async () => { + const {context} = await getTestState(); + + let targetChanged = false; + const listener = () => { + targetChanged = true; + }; + context.on('targetchanged', listener); + const targetPromise = waitEvent<Target>(context, 'targetcreated'); + const newPagePromise = context.newPage(); + const target = await targetPromise; + expect(target.url()).toBe('about:blank'); + + const newPage = await newPagePromise; + const targetPromise2 = waitEvent<Target>(context, 'targetcreated'); + const evaluatePromise = newPage.evaluate(() => { + return window.open('about:blank'); + }); + const target2 = await targetPromise2; + expect(target2.url()).toBe('about:blank'); + await evaluatePromise; + await newPage.close(); + expect(targetChanged).toBe(false); + context.off('targetchanged', listener); + }); + + it('should not crash while redirecting if original request was missed', async () => { + const {page, server, context} = await getTestState(); + + let serverResponse!: ServerResponse; + server.setRoute('/one-style.css', (_req, res) => { + return (serverResponse = res); + }); + // Open a new page. Use window.open to connect to the page later. + await Promise.all([ + page.evaluate((url: string) => { + return window.open(url); + }, server.PREFIX + '/one-style.html'), + server.waitForRequest('/one-style.css'), + ]); + // Connect to the opened page. + const target = await context.waitForTarget( + target => { + return target.url().includes('one-style.html'); + }, + {timeout: 3000} + ); + const newPage = (await target.page())!; + const loadEvent = waitEvent(newPage, 'load'); + // Issue a redirect. + serverResponse.writeHead(302, {location: '/injectedstyle.css'}); + serverResponse.end(); + // Wait for the new page to load. + await loadEvent; + // Cleanup. + await newPage.close(); + }); + it('should have an opener', async () => { + const {page, server, context} = await getTestState(); + + await page.goto(server.EMPTY_PAGE); + const [createdTarget] = await Promise.all([ + waitEvent<Target>(context, 'targetcreated'), + page.goto(server.PREFIX + '/popup/window-open.html'), + ]); + expect((await createdTarget.page())!.url()).toBe( + server.PREFIX + '/popup/popup.html' + ); + expect(createdTarget.opener()).toBe(page.target()); + expect(page.target().opener()).toBeUndefined(); + }); + + describe('Browser.waitForTarget', () => { + it('should wait for a target', async () => { + const {browser, server} = await getTestState(); + + let resolved = false; + const targetPromise = browser.waitForTarget( + target => { + return target.url() === server.EMPTY_PAGE; + }, + {timeout: 3000} + ); + targetPromise + .then(() => { + return (resolved = true); + }) + .catch(error => { + resolved = true; + if (error instanceof TimeoutError) { + console.error(error); + } else { + throw error; + } + }); + const page = await browser.newPage(); + expect(resolved).toBe(false); + await page.goto(server.EMPTY_PAGE); + try { + const target = await targetPromise; + expect(await target.page()).toBe(page); + } catch (error) { + if (error instanceof TimeoutError) { + console.error(error); + } else { + throw error; + } + } + await page.close(); + }); + it('should timeout waiting for a non-existent target', async () => { + const {browser, server} = await getTestState(); + + let error!: Error; + await browser + .waitForTarget( + target => { + return target.url() === server.PREFIX + '/does-not-exist.html'; + }, + { + timeout: 1, + } + ) + .catch(error_ => { + return (error = error_); + }); + expect(error).toBeInstanceOf(TimeoutError); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/touchscreen.spec.ts b/remote/test/puppeteer/test/src/touchscreen.spec.ts new file mode 100644 index 0000000000..28a18ec449 --- /dev/null +++ b/remote/test/puppeteer/test/src/touchscreen.spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2018 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; + +declare const allEvents: Array<{type: string}>; + +describe('Touchscreen', () => { + setupTestBrowserHooks(); + + describe('Touchscreen.prototype.tap', () => { + it('should work', async () => { + const {page, server, isHeadless} = await getTestState(); + await page.goto(server.PREFIX + '/input/touchscreen.html'); + + await page.tap('button'); + expect( + ( + await page.evaluate(() => { + return allEvents; + }) + ).filter(({type}) => { + return type !== 'pointermove' || isHeadless; + }) + ).toMatchObject([ + {height: 1, type: 'pointerdown', width: 1, x: 5, y: 5}, + {touches: [[5, 5, 0.5, 0.5]], type: 'touchstart'}, + {height: 1, type: 'pointerup', width: 1, x: 5, y: 5}, + {touches: [[5, 5, 0.5, 0.5]], type: 'touchend'}, + {height: 1, type: 'click', width: 1, x: 5, y: 5}, + ]); + }); + }); + + describe('Touchscreen.prototype.touchMove', () => { + it('should work', async () => { + const {page, server, isHeadless} = await getTestState(); + await page.goto(server.PREFIX + '/input/touchscreen.html'); + + await page.touchscreen.touchStart(0, 0); + await page.touchscreen.touchMove(10, 10); + await page.touchscreen.touchMove(15.5, 15); + await page.touchscreen.touchMove(20, 20.4); + await page.touchscreen.touchMove(40, 30); + await page.touchscreen.touchEnd(); + expect( + ( + await page.evaluate(() => { + return allEvents; + }) + ).filter(({type}) => { + return type !== 'pointermove' || isHeadless; + }) + ).toMatchObject( + [ + {type: 'pointerdown', x: 0, y: 0, width: 1, height: 1}, + {type: 'touchstart', touches: [[0, 0, 0.5, 0.5]]}, + {type: 'pointermove', x: 10, y: 10, width: 1, height: 1}, + {type: 'touchmove', touches: [[10, 10, 0.5, 0.5]]}, + {type: 'pointermove', x: 16, y: 15, width: 1, height: 1}, + {type: 'touchmove', touches: [[16, 15, 0.5, 0.5]]}, + {type: 'pointermove', x: 20, y: 20, width: 1, height: 1}, + {type: 'touchmove', touches: [[20, 20, 0.5, 0.5]]}, + {type: 'pointermove', x: 40, y: 30, width: 1, height: 1}, + {type: 'touchmove', touches: [[40, 30, 0.5, 0.5]]}, + {type: 'pointerup', x: 40, y: 30, width: 1, height: 1}, + {type: 'touchend', touches: [[40, 30, 0.5, 0.5]]}, + ].filter(({type}) => { + return type !== 'pointermove' || isHeadless; + }) + ); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/tracing.spec.ts b/remote/test/puppeteer/test/src/tracing.spec.ts new file mode 100644 index 0000000000..2c0a5aff19 --- /dev/null +++ b/remote/test/puppeteer/test/src/tracing.spec.ts @@ -0,0 +1,149 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import fs from 'fs'; +import path from 'path'; + +import expect from 'expect'; + +import {launch} from './mocha-utils.js'; + +describe('Tracing', function () { + let outputFile!: string; + let testState: Awaited<ReturnType<typeof launch>>; + + /* we manually manage the browser here as we want a new browser for each + * individual test, which isn't the default behaviour of getTestState() + */ + beforeEach(async () => { + testState = await launch({}); + outputFile = path.join(__dirname, 'trace.json'); + }); + + afterEach(async () => { + await testState.close(); + if (fs.existsSync(outputFile)) { + fs.unlinkSync(outputFile); + } + }); + + it('should output a trace', async () => { + const {server, page} = testState; + await page.tracing.start({screenshots: true, path: outputFile}); + await page.goto(server.PREFIX + '/grid.html'); + await page.tracing.stop(); + expect(fs.existsSync(outputFile)).toBe(true); + }); + + it('should run with custom categories if provided', async () => { + const {page} = testState; + await page.tracing.start({ + path: outputFile, + categories: ['-*', 'disabled-by-default-devtools.timeline.frame'], + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, {encoding: 'utf8'}) + ); + const traceConfig = JSON.parse(traceJson.metadata['trace-config']); + expect(traceConfig.included_categories).toEqual([ + 'disabled-by-default-devtools.timeline.frame', + ]); + expect(traceConfig.excluded_categories).toEqual(['*']); + expect(traceJson.traceEvents).not.toContainEqual( + expect.objectContaining({ + cat: 'toplevel', + }) + ); + }); + + it('should run with default categories', async () => { + const {page} = testState; + await page.tracing.start({ + path: outputFile, + }); + await page.tracing.stop(); + + const traceJson = JSON.parse( + fs.readFileSync(outputFile, {encoding: 'utf8'}) + ); + expect(traceJson.traceEvents).toContainEqual( + expect.objectContaining({ + cat: 'toplevel', + }) + ); + }); + it('should throw if tracing on two pages', async () => { + const {page, browser} = testState; + await page.tracing.start({path: outputFile}); + const newPage = await browser.newPage(); + let error!: Error; + await newPage.tracing.start({path: outputFile}).catch(error_ => { + return (error = error_); + }); + await newPage.close(); + expect(error).toBeTruthy(); + await page.tracing.stop(); + }); + it('should return a buffer', async () => { + const {page, server} = testState; + + await page.tracing.start({screenshots: true, path: outputFile}); + await page.goto(server.PREFIX + '/grid.html'); + const trace = (await page.tracing.stop())!; + const buf = fs.readFileSync(outputFile); + expect(trace.toString()).toEqual(buf.toString()); + }); + it('should work without options', async () => { + const {page, server} = testState; + + await page.tracing.start(); + await page.goto(server.PREFIX + '/grid.html'); + const trace = await page.tracing.stop(); + expect(trace).toBeTruthy(); + }); + + it('should return undefined in case of Buffer error', async () => { + const {page, server} = testState; + + await page.tracing.start({screenshots: true}); + await page.goto(server.PREFIX + '/grid.html'); + + const oldBufferConcat = Buffer.concat; + try { + Buffer.concat = () => { + throw new Error('error'); + }; + const trace = await page.tracing.stop(); + expect(trace).toEqual(undefined); + } finally { + Buffer.concat = oldBufferConcat; + } + }); + + it('should support a buffer without a path', async () => { + const {page, server} = testState; + + await page.tracing.start({screenshots: true}); + await page.goto(server.PREFIX + '/grid.html'); + const trace = (await page.tracing.stop())!; + expect(trace.toString()).toContain('screenshot'); + }); + + it('should properly fail if readProtocolStream errors out', async () => { + const {page} = testState; + await page.tracing.start({path: __dirname}); + + let error!: Error; + try { + await page.tracing.stop(); + } catch (error_) { + error = error_ as Error; + } + expect(error).toBeDefined(); + }); +}); diff --git a/remote/test/puppeteer/test/src/utils.ts b/remote/test/puppeteer/test/src/utils.ts new file mode 100644 index 0000000000..d1bad65a16 --- /dev/null +++ b/remote/test/puppeteer/test/src/utils.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright 2017 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import {rm} from 'fs/promises'; +import {tmpdir} from 'os'; +import path from 'path'; + +import expect from 'expect'; +import type {Frame} from 'puppeteer-core/internal/api/Frame.js'; +import type {Page} from 'puppeteer-core/internal/api/Page.js'; +import type {EventEmitter} from 'puppeteer-core/internal/common/EventEmitter.js'; +import {Deferred} from 'puppeteer-core/internal/util/Deferred.js'; + +import {compare} from './golden-utils.js'; + +const PROJECT_ROOT = path.join(__dirname, '..', '..'); + +declare module 'expect' { + interface Matchers<R> { + toBeGolden(pathOrBuffer: string | Buffer): R; + } +} + +export const extendExpectWithToBeGolden = ( + goldenDir: string, + outputDir: string +): void => { + expect.extend({ + toBeGolden: (testScreenshot: string | Buffer, goldenFilePath: string) => { + const result = compare( + goldenDir, + outputDir, + testScreenshot, + goldenFilePath + ); + + if (result.pass) { + return { + pass: true, + message: () => { + return ''; + }, + }; + } else { + return { + pass: false, + message: () => { + return result.message; + }, + }; + } + }, + }); +}; + +export const projectRoot = (): string => { + return PROJECT_ROOT; +}; + +export const attachFrame = async ( + pageOrFrame: Page | Frame, + frameId: string, + url: string +): Promise<Frame | undefined> => { + using handle = await pageOrFrame.evaluateHandle(attachFrame, frameId, url); + return (await handle.asElement()?.contentFrame()) ?? undefined; + + async function attachFrame(frameId: string, url: string) { + const frame = document.createElement('iframe'); + frame.src = url; + frame.id = frameId; + document.body.appendChild(frame); + await new Promise(x => { + return (frame.onload = x); + }); + return frame; + } +}; + +export const isFavicon = (request: {url: () => string | string[]}): boolean => { + return request.url().includes('favicon.ico'); +}; + +export async function detachFrame( + pageOrFrame: Page | Frame, + frameId: string +): Promise<void> { + await pageOrFrame.evaluate(detachFrame, frameId); + + function detachFrame(frameId: string) { + const frame = document.getElementById(frameId) as HTMLIFrameElement; + frame.remove(); + } +} + +export async function navigateFrame( + pageOrFrame: Page | Frame, + frameId: string, + url: string +): Promise<void> { + await pageOrFrame.evaluate(navigateFrame, frameId, url); + + function navigateFrame(frameId: string, url: string) { + const frame = document.getElementById(frameId) as HTMLIFrameElement; + frame.src = url; + return new Promise(x => { + return (frame.onload = x); + }); + } +} + +export const dumpFrames = (frame: Frame, indentation?: string): string[] => { + indentation = indentation || ''; + let description = frame.url().replace(/:\d{4,5}\//, ':<PORT>/'); + if (frame.name()) { + description += ' (' + frame.name() + ')'; + } + const result = [indentation + description]; + for (const child of frame.childFrames()) { + result.push(...dumpFrames(child, ' ' + indentation)); + } + return result; +}; + +export const waitEvent = async <T = any>( + emitter: EventEmitter<any>, + eventName: string, + predicate: (event: T) => boolean = () => { + return true; + } +): Promise<T> => { + const deferred = Deferred.create<T>({ + timeout: 5000, + message: `Waiting for ${eventName} event timed out.`, + }); + const handler = (event: T) => { + if (!predicate(event)) { + return; + } + deferred.resolve(event); + }; + emitter.on(eventName, handler); + try { + return await deferred.valueOrThrow(); + } finally { + emitter.off(eventName, handler); + } +}; + +export interface FilePlaceholder { + filename: `${string}.webm`; + [Symbol.dispose](): void; +} + +export function getUniqueVideoFilePlaceholder(): FilePlaceholder { + return { + filename: `${tmpdir()}/test-video-${Math.round( + Math.random() * 10000 + )}.webm`, + [Symbol.dispose]() { + void rmIfExists(this.filename); + }, + }; +} + +export function rmIfExists(file: string): Promise<void> { + return rm(file).catch(() => {}); +} diff --git a/remote/test/puppeteer/test/src/waittask.spec.ts b/remote/test/puppeteer/test/src/waittask.spec.ts new file mode 100644 index 0000000000..8ff52db16f --- /dev/null +++ b/remote/test/puppeteer/test/src/waittask.spec.ts @@ -0,0 +1,867 @@ +/** + * @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, + setupTestBrowserHooks, +} from './mocha-utils.js'; +import {attachFrame, detachFrame} 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('<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: HTMLElement) => { + 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/); + }); + }); + + describe('Page.waitForTimeout', () => { + it('waits for the given timeout before resolving', async () => { + const {page, server} = await 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} = await 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} = 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. + 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} = 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; + }); + + 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).toContain( + 'waitForFunction failed: frame got detached.' + ); + }); + 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('<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 (visibility)', async () => { + const {page} = await getTestState(); + + const promise = page.waitForSelector('div', {visible: true}); + await page.setContent('<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('<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( + `<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(`<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(`<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('<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(`<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).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} = await 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} = await 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} = await 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} = 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: 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} = await 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} = await 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} = 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 waitForXPathPromise = frame2.waitForXPath('//div'); + await frame1.evaluate(addElement, 'div'); + await frame2.evaluate(addElement, 'div'); + using eHandle = await waitForXPathPromise; + 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 + .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} = await 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} = await getTestState(); + + using 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} = await getTestState(); + + await page.setContent(`<div style='display: none;'>text</div>`); + + using waitForXPath = await page.waitForXPath('//div', {hidden: true}); + + expect(waitForXPath).toBeInstanceOf(ElementHandle); + }); + it('should return the element handle', async () => { + const {page} = await 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} = await getTestState(); + + await page.setContent(`<div>some text</div>`); + using 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} = await 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'); + }); + }); +}); diff --git a/remote/test/puppeteer/test/src/worker.spec.ts b/remote/test/puppeteer/test/src/worker.spec.ts new file mode 100644 index 0000000000..254ff4a514 --- /dev/null +++ b/remote/test/puppeteer/test/src/worker.spec.ts @@ -0,0 +1,109 @@ +/** + * @license + * Copyright 2020 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import expect from 'expect'; +import type {WebWorker} from 'puppeteer-core/internal/api/WebWorker.js'; +import type {ConsoleMessage} from 'puppeteer-core/internal/common/ConsoleMessage.js'; + +import {getTestState, setupTestBrowserHooks} from './mocha-utils.js'; +import {waitEvent} from './utils.js'; + +describe('Workers', function () { + setupTestBrowserHooks(); + + it('Page.workers', async () => { + const {page, server} = await getTestState(); + + await Promise.all([ + waitEvent(page, 'workercreated'), + page.goto(server.PREFIX + '/worker/worker.html'), + ]); + const worker = page.workers()[0]!; + expect(worker?.url()).toContain('worker.js'); + + expect( + await worker?.evaluate(() => { + return (globalThis as any).workerFunction(); + }) + ).toBe('worker function result'); + + await page.goto(server.EMPTY_PAGE); + expect(page.workers()).toHaveLength(0); + }); + it('should emit created and destroyed events', async () => { + const {page} = await getTestState(); + + const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated'); + using workerObj = await page.evaluateHandle(() => { + return new Worker('data:text/javascript,1'); + }); + const worker = await workerCreatedPromise; + using workerThisObj = await worker.evaluateHandle(() => { + return this; + }); + const workerDestroyedPromise = waitEvent(page, 'workerdestroyed'); + await page.evaluate((workerObj: Worker) => { + return workerObj.terminate(); + }, workerObj); + expect(await workerDestroyedPromise).toBe(worker); + const error = await workerThisObj.getProperty('self').catch(error => { + return error; + }); + expect(error.message).toContain('Most likely the worker has been closed.'); + }); + it('should report console logs', async () => { + const {page} = await getTestState(); + + const [message] = await Promise.all([ + waitEvent(page, 'console'), + page.evaluate(() => { + return new Worker(`data:text/javascript,console.log(1)`); + }), + ]); + expect(message.text()).toBe('1'); + expect(message.location()).toEqual({ + url: '', + lineNumber: 0, + columnNumber: 8, + }); + }); + it('should have JSHandles for console logs', async () => { + const {page} = await getTestState(); + + const logPromise = waitEvent<ConsoleMessage>(page, 'console'); + await page.evaluate(() => { + return new Worker(`data:text/javascript,console.log(1,2,3,this)`); + }); + const log = await logPromise; + expect(log.text()).toBe('1 2 3 JSHandle@object'); + expect(log.args()).toHaveLength(4); + expect(await (await log.args()[3]!.getProperty('origin')).jsonValue()).toBe( + 'null' + ); + }); + it('should have an execution context', async () => { + const {page} = await getTestState(); + + const workerCreatedPromise = waitEvent<WebWorker>(page, 'workercreated'); + await page.evaluate(() => { + return new Worker(`data:text/javascript,console.log(1)`); + }); + const worker = await workerCreatedPromise; + expect(await worker.evaluate('1+1')).toBe(2); + }); + it('should report errors', async () => { + const {page} = await getTestState(); + + const errorPromise = waitEvent<Error>(page, 'pageerror'); + await page.evaluate(() => { + return new Worker( + `data:text/javascript, throw new Error('this is my error');` + ); + }); + const errorLog = await errorPromise; + expect(errorLog.message).toContain('this is my error'); + }); +}); diff --git a/remote/test/puppeteer/test/tsconfig.json b/remote/test/puppeteer/test/tsconfig.json new file mode 100644 index 0000000000..554d034ff1 --- /dev/null +++ b/remote/test/puppeteer/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "build", + "rootDir": "src", + }, + "include": ["src"], +} diff --git a/remote/test/puppeteer/test/tsdoc.json b/remote/test/puppeteer/test/tsdoc.json new file mode 100644 index 0000000000..f5b91f4af6 --- /dev/null +++ b/remote/test/puppeteer/test/tsdoc.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + "tagDefinitions": [ + { + "tagName": "@license", + "syntaxKind": "modifier", + "allowMultiple": false + } + ], + "supportForTags": { + "@license": true + } +} |