diff --git a/.gitignore b/.gitignore index 69d85e4975..aadc481067 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ test-results /tests/installation/output/ /tests/installation/.registry.json .cache/ -.eslintcache \ No newline at end of file +.eslintcache +playwright.env diff --git a/README.md b/README.md index fde98ea6fe..d5a3110613 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 🎭 Playwright -[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-128.0.6613.36-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) +[![npm version](https://img.shields.io/npm/v/playwright.svg)](https://www.npmjs.com/package/playwright) [![Chromium version](https://img.shields.io/badge/chromium-129.0.6668.22-blue.svg?logo=google-chrome)](https://www.chromium.org/Home) [![Firefox version](https://img.shields.io/badge/firefox-129.0-blue.svg?logo=firefoxbrowser)](https://www.mozilla.org/en-US/firefox/new/) [![WebKit version](https://img.shields.io/badge/webkit-18.0-blue.svg?logo=safari)](https://webkit.org/) [![Join Discord](https://img.shields.io/badge/join-discord-infomational)](https://aka.ms/playwright/discord) ## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright) @@ -8,7 +8,7 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 128.0.6613.36 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 129.0.6668.22 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 18.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Firefox 129.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..78cb929fb1 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,17 @@ +# Support + +## How to file issues and get help + +This project uses GitHub issues to track bugs and feature requests. Please search the [existing issues][gh-issues] before filing new ones to avoid duplicates. For new issues, file your bug or feature request as a new issue using corresponding template. + +For help and questions about using this project, please see the [docs site for Playwright][docs]. + +Join our community [Discord Server][discord-server] to connect with other developers using Playwright and ask questions in our 'help-playwright' forum. + +## Microsoft Support Policy + +Support for Playwright is limited to the resources listed above. + +[gh-issues]: https://github.com/microsoft/playwright/issues/ +[docs]: https://playwright.dev/ +[discord-server]: https://aka.ms/playwright/discord diff --git a/docs/src/api/class-apiresponse.md b/docs/src/api/class-apiresponse.md index 1297d2d4fa..5a901b76ba 100644 --- a/docs/src/api/class-apiresponse.md +++ b/docs/src/api/class-apiresponse.md @@ -60,7 +60,7 @@ An object with all the response HTTP headers associated with this response. - `name` <[string]> Name of the header. - `value` <[string]> Value of the header. -An array with all the request HTTP headers associated with this response. Header names are not lower-cased. +An array with all the response HTTP headers associated with this response. Header names are not lower-cased. Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. ## async method: APIResponse.json diff --git a/docs/src/api/class-browser.md b/docs/src/api/class-browser.md index 0c6fd67160..59cf4c99c0 100644 --- a/docs/src/api/class-browser.md +++ b/docs/src/api/class-browser.md @@ -297,8 +297,10 @@ testing frameworks should explicitly create [`method: Browser.newContext`] follo ## async method: Browser.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. ### param: Browser.removeAllListeners.type * since: v1.47 diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index eed27fc1c1..43396f4957 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -6,7 +6,7 @@ BrowserContexts provide a way to operate multiple independent browser sessions. If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser context. -Playwright allows creating "incognito" browser contexts with [`method: Browser.newContext`] method. "Incognito" browser +Playwright allows creating isolated non-persistent browser contexts with [`method: Browser.newContext`] method. Non-persistent browser contexts don't write any browsing data to disk. ```js @@ -415,42 +415,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: -**Bundling** - -If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. - -```js browser title="mocks/mockRandom.ts" -// This script can import other files. -import { defaultValue } from './defaultValue'; - -export default function(value?: number) { - window.Math.random = () => value ?? defaultValue; -} -``` - -```sh -# bundle with esbuild -esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js -``` - -```js title="tests/example.spec.ts" -const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - -// Passing 42 as an argument to the default export function. -await context.addInitScript({ path: mockPath }, 42); - -// Make sure to pass something even if you do not need to pass an argument. -// This instructs Playwright to treat the file as a commonjs module. -await context.addInitScript({ path: mockPath }, ''); -``` - ### param: BrowserContext.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. - - `content` ?<[string]> Raw script content. + current working directory. Optional. + - `content` ?<[string]> Raw script content. Optional. Script to be evaluated in all pages in the browser context. @@ -466,9 +437,7 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional JSON-serializable argument to pass to [`param: script`]. -* When `script` is a function, the argument is passed to it directly. -* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. +Optional argument to pass to [`param: script`] (only supported when passing a function). ### param: BrowserContext.addInitScript.path * since: v1.8 @@ -1048,8 +1017,10 @@ Returns all open pages in the context. ## async method: BrowserContext.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. ### param: BrowserContext.removeAllListeners.type * since: v1.47 diff --git a/docs/src/api/class-elementhandle.md b/docs/src/api/class-elementhandle.md index 1793798c8c..c8f54c7380 100644 --- a/docs/src/api/class-elementhandle.md +++ b/docs/src/api/class-elementhandle.md @@ -866,7 +866,7 @@ await handle.SelectOptionAsync(new[] { ### option: ElementHandle.selectOption.force = %%-input-force-%% * since: v1.13 -### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: ElementHandle.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: ElementHandle.selectOption.timeout = %%-input-timeout-%% diff --git a/docs/src/api/class-frame.md b/docs/src/api/class-frame.md index 4da22fe989..f3f308622f 100644 --- a/docs/src/api/class-frame.md +++ b/docs/src/api/class-frame.md @@ -1543,7 +1543,7 @@ await frame.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" }) ### option: Frame.selectOption.force = %%-input-force-%% * since: v1.13 -### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Frame.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: Frame.selectOption.strict = %%-input-strict-%% diff --git a/docs/src/api/class-locator.md b/docs/src/api/class-locator.md index 8a148ddf0b..4df0035098 100644 --- a/docs/src/api/class-locator.md +++ b/docs/src/api/class-locator.md @@ -2055,7 +2055,7 @@ await element.SelectOptionAsync(new[] { "red", "green", "blue" }); ### option: Locator.selectOption.force = %%-input-force-%% * since: v1.14 -### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Locator.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.14 ### option: Locator.selectOption.timeout = %%-input-timeout-%% diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index 78d38a6f7a..ea5fe74dfa 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -619,42 +619,13 @@ The order of evaluation of multiple scripts installed via [`method: BrowserConte [`method: Page.addInitScript`] is not defined. ::: -**Bundling** - -If you have a complex script split into several files, it needs to be bundled into a single file first. We recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a commonjs module and pass [`option: path`] and [`option: arg`]. - -```js browser title="mocks/mockRandom.ts" -// This script can import other files. -import { defaultValue } from './defaultValue'; - -export default function(value?: number) { - window.Math.random = () => value ?? defaultValue; -} -``` - -```sh -# bundle with esbuild -esbuild mocks/mockRandom.ts --bundle --format=cjs --outfile=mocks/mockRandom.js -``` - -```js title="tests/example.spec.ts" -const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - -// Passing 42 as an argument to the default export function. -await page.addInitScript({ path: mockPath }, 42); - -// Make sure to pass something even if you do not need to pass an argument. -// This instructs Playwright to treat the file as a commonjs module. -await page.addInitScript({ path: mockPath }, ''); -``` - ### param: Page.addInitScript.script * since: v1.8 * langs: js - `script` <[function]|[string]|[Object]> - `path` ?<[path]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to the - current working directory. - - `content` ?<[string]> Raw script content. + current working directory. Optional. + - `content` ?<[string]> Raw script content. Optional. Script to be evaluated in the page. @@ -670,9 +641,7 @@ Script to be evaluated in all pages in the browser context. * langs: js - `arg` ?<[Serializable]> -Optional JSON-serializable argument to pass to [`param: script`]. -* When `script` is a function, the argument is passed to it directly. -* When `script` is a file path, the file is assumed to be a commonjs module. The default export, either `module.exports` or `module.exports.default`, should be a function that's going to be executed with this argument. +Optional argument to pass to [`param: script`] (only supported when passing a function). ### param: Page.addInitScript.path * since: v1.8 @@ -2195,6 +2164,7 @@ A glob pattern, regex pattern or predicate receiving frame's `url` as a [URL] ob ## method: Page.frameLocator * since: v1.17 +regular [`Locator`] instead. - returns: <[FrameLocator]> When working with iframes, you can create a frame locator that will enter the iframe and allow selecting elements @@ -3372,8 +3342,23 @@ By default, after calling the handler Playwright will wait until the overlay bec ## async method: Page.removeAllListeners * since: v1.47 +* langs: js -Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. +Removes all the listeners of the given type (or all registered listeners if no type given). +Allows to wait for async listeners to complete or to ignore subsequent errors from these listeners. + +**Usage** + +```js +page.on('request', async request => { + const response = await request.response(); + const body = await response.body(); + console.log(body.byteLength); +}); +await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); +// Waits for all the reported 'request' events to resolve. +await page.removeAllListeners('request', { behavior: 'wait' }); +``` ### param: Page.removeAllListeners.type * since: v1.47 @@ -3742,7 +3727,7 @@ await page.SelectOptionAsync("select#colors", new[] { "red", "green", "blue" }); ### option: Page.selectOption.force = %%-input-force-%% * since: v1.13 -### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-%% +### option: Page.selectOption.noWaitAfter = %%-input-no-wait-after-removed-%% * since: v1.8 ### option: Page.selectOption.strict = %%-input-strict-%% diff --git a/docs/src/mock.md b/docs/src/mock.md index bd0c4e5c54..87ddf2ec96 100644 --- a/docs/src/mock.md +++ b/docs/src/mock.md @@ -288,7 +288,7 @@ await Expect(page.GetByText("Strawberry")).ToBeVisibleAsync(); ```java // Get the response from the HAR file -page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() +page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions() .setUrl("*/**/api/v1/fruits") .setUpdate(true) ); @@ -386,7 +386,7 @@ await page.ExpectByTextAsync("Playwright", new() { Exact = true }).ToBeVisibleAs // Replay API requests from HAR. // Either use a matching response from the HAR, // or abort the request if nothing matches. -page.routeFromHAR("./hars/fruit.har", new RouteFromHAROptions() +page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions() .setUrl("*/**/api/v1/fruits") .setUpdate(false) ); diff --git a/docs/src/network.md b/docs/src/network.md index c7e6bdca1c..152231556e 100644 --- a/docs/src/network.md +++ b/docs/src/network.md @@ -191,7 +191,7 @@ test('should use custom proxy on a new context', async ({ browser }) => { } }); const page = await context.newPage(); - + await context.close(); }); ``` diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index a2e65e8849..69c080297f 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -722,3 +722,64 @@ export const test = base.extend({ }, { title: 'my fixture' }], }); ``` + +## Adding global beforeEach/afterEach hooks + +[`method: Test.beforeEach`] and [`method: Test.afterEach`] hooks run before/after each test declared in the same file and same [`method: Test.describe`] block (if any). If you want to declare hooks that run before/after each test globally, you can declare them as auto fixtures like this: + +```ts title="fixtures.ts" +import { test as base } from '@playwright/test'; + +export const test = base.extend<{ forEachTest: void }>({ + forEachTest: [async ({ page }, use) => { + // This code runs before every test. + await page.goto('http://localhost:8000'); + await use(); + // This code runs after every test. + console.log('Last URL:', page.url()); + }, { auto: true }], // automatically starts for every test. +}); +``` + +And then import the fixtures in all your tests: + +```ts title="mytest.spec.ts" +import { test } from './fixtures'; +import { expect } from '@playwright/test'; + +test('basic', async ({ page }) => { + expect(page).toHaveURL('http://localhost:8000'); + await page.goto('https://playwright.dev'); +}); +``` + +## Adding global beforeAll/afterAll hooks + +[`method: Test.beforeAll`] and [`method: Test.afterAll`] hooks run before/after all tests declared in the same file and same [`method: Test.describe`] block (if any), once per worker process. If you want to declare hooks +that run before/after all tests in every file, you can declare them as auto fixtures with `scope: 'worker'` as follows: + +```ts title="fixtures.ts" +import { test as base } from '@playwright/test'; + +export const test = base.extend<{}, { forEachWorker: void }>({ + forEachWorker: [async ({}, use) => { + // This code runs before all the tests in the worker process. + console.log(`Starting test worker ${test.info().workerIndex}`); + await use(); + // This code runs after all the tests in the worker process. + console.log(`Stopping test worker ${test.info().workerIndex}`); + }, { scope: 'worker', auto: true }], // automatically starts for every worker. +}); +``` + +And then import the fixtures in all your tests: + +```ts title="mytest.spec.ts" +import { test } from './fixtures'; +import { expect } from '@playwright/test'; + +test('basic', async ({ }) => { + // ... +}); +``` +Note that the fixtures will still run once per [worker process](./test-parallel.md#worker-processes), but you don't need to redeclare them in every file. diff --git a/docs/src/test-typescript-js.md b/docs/src/test-typescript-js.md index 5eaa3670a5..6e18b3c615 100644 --- a/docs/src/test-typescript-js.md +++ b/docs/src/test-typescript-js.md @@ -80,14 +80,14 @@ By default, Playwright will look up a closest tsconfig for each imported file by ```sh # Playwright will choose tsconfig automatically -npx playwrigh test +npx playwright test ``` Alternatively, you can specify a single tsconfig file to use in the command line, and Playwright will use it for all imported files, not only test files. ```sh # Pass a specific tsconfig -npx playwrigh test --tsconfig=tsconfig.test.json +npx playwright test --tsconfig=tsconfig.test.json ``` ## Manually compile tests with TypeScript diff --git a/package-lock.json b/package-lock.json index 1c48c213d7..8edb318925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.18.11", "eslint": "^8.55.0", @@ -3293,15 +3293,15 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/electron": { @@ -6820,9 +6820,9 @@ } }, "node_modules/svelte": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.9.tgz", - "integrity": "sha512-hsoB/WZGEPFXeRRLPhPrbRz67PhP6sqYgvwcAs+gWdSQSvNDw+/lTeUJSWe5h2xC97Fz/8QxAOqItwBzNJPU8w==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -8060,7 +8060,7 @@ "playwright": "cli.js" }, "devDependencies": { - "svelte": "^4.2.8" + "svelte": "^4.2.19" }, "engines": { "node": ">=18" diff --git a/package.json b/package.json index 930fac8a80..6b2e043765 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "colors": "^1.4.0", "concurrently": "^6.2.1", "cross-env": "^7.0.3", - "dotenv": "^16.0.0", + "dotenv": "^16.4.5", "electron": "^30.1.2", "esbuild": "^0.18.11", "eslint": "^8.55.0", diff --git a/packages/html-reporter/src/links.tsx b/packages/html-reporter/src/links.tsx index 419a8725ac..8c1dcc85dc 100644 --- a/packages/html-reporter/src/links.tsx +++ b/packages/html-reporter/src/links.tsx @@ -75,11 +75,16 @@ export const AttachmentLink: React.FunctionComponent<{ attachment: TestAttachment, href?: string, linkName?: string, -}> = ({ attachment, href, linkName }) => { + openInNewTab?: boolean, +}> = ({ attachment, href, linkName, openInNewTab }) => { return {attachment.contentType === kMissingContentType ? icons.warning() : icons.attachment()} {attachment.path && {linkName || attachment.name}} - {!attachment.path && {linkifyText(attachment.name)}} + {!attachment.path && ( + openInNewTab + ? e.stopPropagation()}>{attachment.name} + : {linkifyText(attachment.name)} + )} } loadChildren={attachment.body ? () => { return [
{linkifyText(attachment.body!)}
]; } : undefined} depth={0} style={{ lineHeight: '32px' }}>
; diff --git a/packages/html-reporter/src/testResultView.tsx b/packages/html-reporter/src/testResultView.tsx index 1ec8c65a1e..8ee36d0cda 100644 --- a/packages/html-reporter/src/testResultView.tsx +++ b/packages/html-reporter/src/testResultView.tsx @@ -67,15 +67,16 @@ export const TestResultView: React.FC<{ anchor: 'video' | 'diff' | '', }> = ({ result, anchor }) => { - const { screenshots, videos, traces, otherAttachments, diffs } = React.useMemo(() => { + const { screenshots, videos, traces, otherAttachments, diffs, htmls } = React.useMemo(() => { const attachments = result?.attachments || []; const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/'))); const videos = attachments.filter(a => a.name === 'video'); const traces = attachments.filter(a => a.name === 'trace'); + const htmls = attachments.filter(a => a.contentType.startsWith('text/html')); const otherAttachments = new Set(attachments); - [...screenshots, ...videos, ...traces].forEach(a => otherAttachments.delete(a)); + [...screenshots, ...videos, ...traces, ...htmls].forEach(a => otherAttachments.delete(a)); const diffs = groupImageDiffs(screenshots); - return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs }; + return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, htmls }; }, [result]); const videoRef = React.useRef(null); @@ -135,7 +136,10 @@ export const TestResultView: React.FC<{ )} } - {!!otherAttachments.size && + {!!(otherAttachments.size + htmls.length) && + {[...htmls].map((a, i) => ( + ) + )} {[...otherAttachments].map((a, i) => )} } ; diff --git a/packages/playwright-core/ThirdPartyNotices.txt b/packages/playwright-core/ThirdPartyNotices.txt index 3c5a71e20f..0a3ca6a5f4 100644 --- a/packages/playwright-core/ThirdPartyNotices.txt +++ b/packages/playwright-core/ThirdPartyNotices.txt @@ -16,6 +16,7 @@ This project incorporates components from the projects listed below. The origina - concat-map@0.0.1 (https://github.com/substack/node-concat-map) - debug@4.3.4 (https://github.com/debug-js/debug) - define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop) +- dotenv@16.4.5 (https://github.com/motdotla/dotenv) - end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream) - escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp) - extract-zip@2.0.1 (https://github.com/maxogden/extract-zip) @@ -472,6 +473,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI ========================================= END OF define-lazy-prop@2.0.0 AND INFORMATION +%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE +========================================= +Copyright (c) 2015, Scott Motte +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +========================================= +END OF dotenv@16.4.5 AND INFORMATION + %% end-of-stream@1.4.4 NOTICES AND INFORMATION BEGIN HERE ========================================= The MIT License (MIT) @@ -1514,6 +1543,6 @@ END OF yazl@2.5.1 AND INFORMATION SUMMARY BEGIN HERE ========================================= -Total Packages: 45 +Total Packages: 46 ========================================= END OF SUMMARY \ No newline at end of file diff --git a/packages/playwright-core/browsers.json b/packages/playwright-core/browsers.json index 576b81035d..6d395287a2 100644 --- a/packages/playwright-core/browsers.json +++ b/packages/playwright-core/browsers.json @@ -3,15 +3,15 @@ "browsers": [ { "name": "chromium", - "revision": "1131", + "revision": "1133", "installByDefault": true, - "browserVersion": "128.0.6613.36" + "browserVersion": "129.0.6668.22" }, { "name": "chromium-tip-of-tree", - "revision": "1253", + "revision": "1255", "installByDefault": false, - "browserVersion": "130.0.6670.0" + "browserVersion": "130.0.6684.0" }, { "name": "firefox", @@ -27,7 +27,7 @@ }, { "name": "webkit", - "revision": "2062", + "revision": "2068", "installByDefault": true, "revisionOverrides": { "mac10.14": "1446", diff --git a/packages/playwright-core/bundles/utils/package-lock.json b/packages/playwright-core/bundles/utils/package-lock.json index 66c4cdae12..eef68ef8ee 100644 --- a/packages/playwright-core/bundles/utils/package-lock.json +++ b/packages/playwright-core/bundles/utils/package-lock.json @@ -11,6 +11,7 @@ "colors": "1.4.0", "commander": "8.3.0", "debug": "^4.3.4", + "dotenv": "^16.4.5", "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", @@ -198,6 +199,17 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", @@ -560,6 +572,11 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==" }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, "escape-string-regexp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", diff --git a/packages/playwright-core/bundles/utils/package.json b/packages/playwright-core/bundles/utils/package.json index 8ac0c112fe..a7c66192e0 100644 --- a/packages/playwright-core/bundles/utils/package.json +++ b/packages/playwright-core/bundles/utils/package.json @@ -12,6 +12,7 @@ "colors": "1.4.0", "commander": "8.3.0", "debug": "^4.3.4", + "dotenv": "^16.4.5", "graceful-fs": "4.2.10", "https-proxy-agent": "5.0.0", "jpeg-js": "0.4.4", diff --git a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts index 49dc61a05c..dcb3790629 100644 --- a/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts +++ b/packages/playwright-core/bundles/utils/src/utilsBundleImpl.ts @@ -20,6 +20,9 @@ export const colors = colorsLibrary; import debugLibrary from 'debug'; export const debug = debugLibrary; +import dotenvLibrary from 'dotenv'; +export const dotenv = dotenvLibrary; + export { getProxyForUrl } from 'proxy-from-env'; export { HttpsProxyAgent } from 'https-proxy-agent'; diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts index ad943c049e..9c68271e80 100644 --- a/packages/playwright-core/src/cli/program.ts +++ b/packages/playwright-core/src/cli/program.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import os from 'os'; import path from 'path'; import type { Command } from '../utilsBundle'; -import { program } from '../utilsBundle'; +import { program, dotenv } from '../utilsBundle'; export { program } from '../utilsBundle'; import { runDriver, runServer, printApiJson, launchBrowserServer } from './driver'; import { runTraceInBrowser, runTraceViewerApp } from '../server/trace/viewer/traceViewer'; @@ -561,6 +561,7 @@ async function open(options: Options, url: string | undefined, language: string) async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) { const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options; const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH); + dotenv.config({ path: 'playwright.env' }); await context._enableRecorder({ language, launchOptions, @@ -570,7 +571,6 @@ async function codegen(options: Options & { target: string, output?: string, tes mode: 'recording', testIdAttributeName, outputFile: outputFile ? path.resolve(outputFile) : undefined, - handleSIGINT: false, }); await openPage(context, url); } diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index c4f7827840..ef222136dd 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -481,7 +481,6 @@ export class BrowserContext extends ChannelOwner mode?: 'recording' | 'inspecting', testIdAttributeName?: string, outputFile?: string, - handleSIGINT?: boolean, }) { await this._channel.recorderSupplementEnable(params); } diff --git a/packages/playwright-core/src/client/clientHelper.ts b/packages/playwright-core/src/client/clientHelper.ts index 793219f10b..540230a4fc 100644 --- a/packages/playwright-core/src/client/clientHelper.ts +++ b/packages/playwright-core/src/client/clientHelper.ts @@ -28,37 +28,20 @@ export function envObjectToArray(env: types.Env): { name: string, value: string return result; } -export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg: any, addSourceUrl: boolean = true): Promise { +export async function evaluationScript(fun: Function | string | { path?: string, content?: string }, arg?: any, addSourceUrl: boolean = true): Promise { if (typeof fun === 'function') { const source = fun.toString(); const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg); return `(${source})(${argString})`; } - if (isString(fun)) { - if (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); + if (arg !== undefined) + throw new Error('Cannot evaluate a string with arguments'); + if (isString(fun)) return fun; - } - if (fun.content !== undefined) { - if (arg !== undefined) - throw new Error('Cannot evaluate a string with arguments'); + if (fun.content !== undefined) return fun.content; - } if (fun.path !== undefined) { let source = await fs.promises.readFile(fun.path, 'utf8'); - if (arg !== undefined) { - // Assume a CJS module that has a function default export. - source = `(() => { - var exports = {}; var module = { exports }; - ${source} - let __pw_result__ = module.exports; - if (__pw_result__ && typeof __pw_result__ === 'object' && ('default' in __pw_result__)) - __pw_result__ = __pw_result__['default']; - if (typeof __pw_result__ !== 'function') - return __pw_result__; - return __pw_result__(${JSON.stringify(arg)}); - })()`; - } if (addSourceUrl) source = addSourceUrlToScript(source, fun.path); return source; diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index 37d374e3ec..2e7f7e4107 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -33,7 +33,7 @@ export type WaitForEventOptions = Function | { predicate?: Function, timeout?: n export type WaitForFunctionOptions = { timeout?: number, polling?: 'raf' | number }; export type SelectOption = { value?: string, label?: string, index?: number, valueOrLabel?: string }; -export type SelectOptionOptions = { force?: boolean, timeout?: number, noWaitAfter?: boolean }; +export type SelectOptionOptions = { force?: boolean, timeout?: number }; export type FilePayload = { name: string, mimeType: string, buffer: Buffer }; export type StorageState = { cookies: channels.NetworkCookie[], diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 81755c79bc..1768380d30 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -961,7 +961,6 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({ device: tOptional(tString), saveStorage: tOptional(tString), outputFile: tOptional(tString), - handleSIGINT: tOptional(tBoolean), omitCallTracking: tOptional(tBoolean), }); scheme.BrowserContextRecorderSupplementEnableResult = tOptional(tObject({})); @@ -1637,7 +1636,6 @@ scheme.FrameSelectOptionParams = tObject({ }))), force: tOptional(tBoolean), timeout: tOptional(tNumber), - noWaitAfter: tOptional(tBoolean), }); scheme.FrameSelectOptionResult = tObject({ values: tArray(tString), @@ -2001,7 +1999,6 @@ scheme.ElementHandleSelectOptionParams = tObject({ }))), force: tOptional(tBoolean), timeout: tOptional(tNumber), - noWaitAfter: tOptional(tBoolean), }); scheme.ElementHandleSelectOptionResult = tObject({ values: tArray(tString), diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index db20728904..8ddbe68f89 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -43,6 +43,7 @@ import { BrowserContextAPIRequestContext } from './fetch'; import type { Artifact } from './artifact'; import { Clock } from './clock'; import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; +import { RecorderApp } from './recorder/recorderApp'; export abstract class BrowserContext extends SdkObject { static Events = { @@ -130,19 +131,21 @@ export abstract class BrowserContext extends SdkObject { // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') - await Recorder.show(this, { pauseOnNextStatement: true }); + await Recorder.show(this, RecorderApp.factory(this), { pauseOnNextStatement: true }); // When paused, show inspector. if (this._debugger.isPaused()) - Recorder.showInspector(this); + Recorder.showInspector(this, RecorderApp.factory(this)); + this._debugger.on(Debugger.Events.PausedStateChanged, () => { - Recorder.showInspector(this); + if (this._debugger.isPaused()) + Recorder.showInspector(this, RecorderApp.factory(this)); }); if (debugMode() === 'console') await this.extendInjectedScript(consoleApiSource.source); if (this._options.serviceWorkers === 'block') - await this.addInitScript(`\nnavigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); + await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); if (this._options.permissions) await this.grantPermissions(this._options.permissions); diff --git a/packages/playwright-core/src/server/chromium/crNetworkManager.ts b/packages/playwright-core/src/server/chromium/crNetworkManager.ts index 850f466afa..a8ff5a08dc 100644 --- a/packages/playwright-core/src/server/chromium/crNetworkManager.ts +++ b/packages/playwright-core/src/server/chromium/crNetworkManager.ts @@ -609,7 +609,7 @@ class RouteImpl implements network.RouteDelegate { this._interceptionId = interceptionId; } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides): Promise { + async continue(overrides: types.NormalizedContinueOverrides): Promise { this._alreadyContinuedParams = { requestId: this._interceptionId!, url: overrides.url, diff --git a/packages/playwright-core/src/server/chromium/protocol.d.ts b/packages/playwright-core/src/server/chromium/protocol.d.ts index 99ce1d3a6a..caadb2a577 100644 --- a/packages/playwright-core/src/server/chromium/protocol.d.ts +++ b/packages/playwright-core/src/server/chromium/protocol.d.ts @@ -1131,17 +1131,21 @@ using Audits.issueAdded event. } /** - * Defines commands and events for browser extensions. Available if the client -is connected using the --remote-debugging-pipe flag and -the --enable-unsafe-extension-debugging flag is set. + * Defines commands and events for browser extensions. */ export module Extensions { + /** + * Storage areas. + */ + export type StorageArea = "session"|"local"|"sync"|"managed"; /** * Installs an unpacked extension from the filesystem similar to --load-extension CLI flags. Returns extension ID once the extension -has been installed. +has been installed. Available if the client is connected using the +--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging +flag is set. */ export type loadUnpackedParameters = { /** @@ -1155,6 +1159,81 @@ has been installed. */ id: string; } + /** + * Gets data from extension storage in the given `storageArea`. If `keys` is +specified, these are used to filter the result. + */ + export type getStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to retrieve data from. + */ + storageArea: StorageArea; + /** + * Keys to retrieve. + */ + keys?: string[]; + } + export type getStorageItemsReturnValue = { + data: { [key: string]: string }; + } + /** + * Removes `keys` from extension storage in the given `storageArea`. + */ + export type removeStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + /** + * Keys to remove. + */ + keys: string[]; + } + export type removeStorageItemsReturnValue = { + } + /** + * Clears extension storage in the given `storageArea`. + */ + export type clearStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + } + export type clearStorageItemsReturnValue = { + } + /** + * Sets `values` in extension storage in the given `storageArea`. The provided `values` +will be merged with existing values in the storage area. + */ + export type setStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to set data in. + */ + storageArea: StorageArea; + /** + * Values to set. + */ + values: { [key: string]: string }; + } + export type setStorageItemsReturnValue = { + } } /** @@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from. */ style: CSSStyle; } - /** - * CSS position-fallback rule representation. - */ - export interface CSSPositionFallbackRule { - name: Value; - /** - * List of keyframes. - */ - tryRules: CSSTryRule[]; - } /** * CSS @position-try rule representation. */ @@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`. * A list of CSS keyframed animations matching this node. */ cssKeyframesRules?: CSSKeyframesRule[]; - /** - * A list of CSS position fallbacks matching this node. - */ - cssPositionFallbackRules?: CSSPositionFallbackRule[]; /** * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. */ @@ -3496,7 +3561,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -3646,6 +3711,13 @@ The property is always undefined now. compatibilityMode?: CompatibilityMode; assignedSlot?: BackendNode; } + /** + * A structure to hold the top-level node of a detached tree and an array of its retained descendants. + */ + export interface DetachedElementInfo { + treeNode: Node; + retainedNodeIds: NodeId[]; + } /** * A structure holding an RGBA color. */ @@ -4693,6 +4765,17 @@ File wrapper. export type getFileInfoReturnValue = { path: string; } + /** + * Returns list of detached nodes + */ + export type getDetachedDomNodesParameters = { + } + export type getDetachedDomNodesReturnValue = { + /** + * The list of detached nodes + */ + detachedNodes: DetachedElementInfo[]; + } /** * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). @@ -11369,7 +11452,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com" */ fixed?: number; } - export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; + export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export interface InstallabilityErrorArgument { /** @@ -12298,6 +12381,10 @@ when bfcache navigation fails. * Frame's new url. */ url: string; + /** + * Navigation type + */ + navigationType: "fragment"|"historyApi"|"other"; } /** * Compressed image data requested by the `startScreencast`. @@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17270,6 +17357,101 @@ supported yet. } } + /** + * This domain allows configuring virtual Bluetooth devices to test +the web-bluetooth API. + */ + export module BluetoothEmulation { + /** + * Indicates the various states of Central. + */ + export type CentralState = "absent"|"powered-off"|"powered-on"; + /** + * Stores the manufacturer data + */ + export interface ManufacturerData { + /** + * Company identifier +https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +https://usb.org/developers + */ + key: number; + /** + * Manufacturer-specific data + */ + data: binary; + } + /** + * Stores the byte data of the advertisement packet sent by a Bluetooth device. + */ + export interface ScanRecord { + name?: string; + uuids?: string[]; + /** + * Stores the external appearance description of the device. + */ + appearance?: number; + /** + * Stores the transmission power of a broadcasting device. + */ + txPower?: number; + /** + * Key is the company identifier and the value is an array of bytes of +manufacturer specific data. + */ + manufacturerData?: ManufacturerData[]; + } + /** + * Stores the advertisement packet information that is sent by a Bluetooth device. + */ + export interface ScanEntry { + deviceAddress: string; + rssi: number; + scanRecord: ScanRecord; + } + + + /** + * Enable the BluetoothEmulation domain. + */ + export type enableParameters = { + /** + * State of the simulated central. + */ + state: CentralState; + } + export type enableReturnValue = { + } + /** + * Disable the BluetoothEmulation domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Simulates a peripheral with |address|, |name| and |knownServiceUuids| +that has already been connected to the system. + */ + export type simulatePreconnectedPeripheralParameters = { + address: string; + name: string; + manufacturerData: ManufacturerData[]; + knownServiceUuids: string[]; + } + export type simulatePreconnectedPeripheralReturnValue = { + } + /** + * Simulates an advertisement packet described in |entry| being received by +the central. + */ + export type simulateAdvertisementParameters = { + entry: ScanEntry; + } + export type simulateAdvertisementReturnValue = { + } + } + /** * This domain is deprecated - use Runtime or Log instead. */ @@ -20122,6 +20304,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; + "Extensions.getStorageItems": Extensions.getStorageItemsParameters; + "Extensions.removeStorageItems": Extensions.removeStorageItemsParameters; + "Extensions.clearStorageItems": Extensions.clearStorageItemsParameters; + "Extensions.setStorageItems": Extensions.setStorageItemsParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -20232,6 +20418,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getFileInfo": DOM.getFileInfoParameters; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeValue": DOM.setNodeValueParameters; @@ -20616,6 +20803,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; + "BluetoothEmulation.enable": BluetoothEmulation.enableParameters; + "BluetoothEmulation.disable": BluetoothEmulation.disableParameters; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20722,6 +20913,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; + "Extensions.getStorageItems": Extensions.getStorageItemsReturnValue; + "Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue; + "Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue; + "Extensions.setStorageItems": Extensions.setStorageItemsReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20832,6 +21027,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue; @@ -21216,6 +21412,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; + "BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue; + "BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; diff --git a/packages/playwright-core/src/server/codegen/DEPS.list b/packages/playwright-core/src/server/codegen/DEPS.list new file mode 100644 index 0000000000..58432390fd --- /dev/null +++ b/packages/playwright-core/src/server/codegen/DEPS.list @@ -0,0 +1,3 @@ +[*] +../../utils/ +../deviceDescriptors.ts diff --git a/packages/playwright-core/src/server/recorder/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts similarity index 90% rename from packages/playwright-core/src/server/recorder/csharp.ts rename to packages/playwright-core/src/server/codegen/csharp.ts index 52460f8121..f11435a0c2 100644 --- a/packages/playwright-core/src/server/recorder/csharp.ts +++ b/packages/playwright-core/src/server/codegen/csharp.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; -import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { BrowserContextOptions } from '../../../types/types'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; +import { sanitizeDeviceOptions, toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { escapeWithQuotes, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -72,14 +68,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.FrameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.ContentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { @@ -93,7 +83,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { } const lines: string[] = []; - lines.push(this._generateActionCall(subject, action)); + lines.push(this._generateActionCall(subject, actionInContext)); if (signals.download) { lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`); @@ -111,7 +101,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -121,16 +112,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { let method = 'Click'; if (action.clickCount === 2) method = 'DblClick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); if (!Object.entries(options).length) return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`; const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options'); @@ -145,7 +127,7 @@ export class CSharpLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.SetInputFilesAsync(${formatObject(action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.PressAsync(${quote(shortcut)});`; } diff --git a/packages/playwright-core/src/server/recorder/java.ts b/packages/playwright-core/src/server/codegen/java.ts similarity index 88% rename from packages/playwright-core/src/server/recorder/java.ts rename to packages/playwright-core/src/server/codegen/java.ts index 72d5d9a995..47c6fa3619 100644 --- a/packages/playwright-core/src/server/recorder/java.ts +++ b/packages/playwright-core/src/server/codegen/java.ts @@ -14,13 +14,10 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { toSignalMap } from './language'; -import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { BrowserContextOptions } from '../../../types/types'; +import type * as types from '../types'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; +import { toClickOptions, toKeyboardModifiers, toSignalMap } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { JavaScriptFormatter } from './javascript'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -63,16 +60,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - let inFrameLocator = false; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - inFrameLocator = true; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector, false)}.contentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { @@ -82,7 +71,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { });`); } - let code = this._generateActionCall(subject, action, inFrameLocator); + let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length); if (signals.popup) { code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> { @@ -101,7 +90,8 @@ export class JavaLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action, inFrameLocator: boolean): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext, inFrameLocator: boolean): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -111,16 +101,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsText = formatClickOptions(options); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`; } @@ -133,7 +114,7 @@ export class JavaLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.setInputFiles(${formatPath(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.press(${quote(shortcut)});`; } @@ -279,7 +260,7 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: return lines.join('\n'); } -function formatClickOptions(options: MouseClickOptions) { +function formatClickOptions(options: types.MouseClickOptions) { const lines = []; if (options.button) lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`); diff --git a/packages/playwright-core/src/server/recorder/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts similarity index 88% rename from packages/playwright-core/src/server/recorder/javascript.ts rename to packages/playwright-core/src/server/codegen/javascript.ts index 104b3bcd53..1c1ba3f1cb 100644 --- a/packages/playwright-core/src/server/recorder/javascript.ts +++ b/packages/playwright-core/src/server/codegen/javascript.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; -import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { BrowserContextOptions } from '../../../types/types'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { deviceDescriptors } from '../deviceDescriptors'; import { escapeWithQuotes, asLocator } from '../../utils'; @@ -52,14 +48,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frameLocator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.contentFrame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) { @@ -74,7 +64,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { if (signals.download) formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`); - formatter.add(this._generateActionCall(subject, action)); + formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext))); if (signals.popup) formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`); @@ -84,7 +74,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -94,16 +85,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`; } @@ -116,7 +98,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`; } @@ -276,3 +258,9 @@ export class JavaScriptFormatter { function quote(text: string) { return escapeWithQuotes(text, '\''); } + +function wrapWithStep(description: string | undefined, body: string) { + return description ? `await test.step(\`${description}\`, async () => { +${body} +});` : body; +} diff --git a/packages/playwright-core/src/server/recorder/jsonl.ts b/packages/playwright-core/src/server/codegen/jsonl.ts similarity index 90% rename from packages/playwright-core/src/server/recorder/jsonl.ts rename to packages/playwright-core/src/server/codegen/jsonl.ts index 108d5eadc6..78485297b6 100644 --- a/packages/playwright-core/src/server/recorder/jsonl.ts +++ b/packages/playwright-core/src/server/codegen/jsonl.ts @@ -15,8 +15,7 @@ */ import { asLocator } from '../../utils'; -import type { ActionInContext } from './codeGenerator'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; export class JsonlLanguageGenerator implements LanguageGenerator { id = 'jsonl'; diff --git a/packages/playwright-core/src/server/codegen/language.ts b/packages/playwright-core/src/server/codegen/language.ts new file mode 100644 index 0000000000..72cfb9083d --- /dev/null +++ b/packages/playwright-core/src/server/codegen/language.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BrowserContextOptions } from '../../..'; +import type * as actions from '../recorder/recorderActions'; +import type * as types from '../types'; +import type { ActionInContext, LanguageGenerator, LanguageGeneratorOptions } from './types'; + +export function generateCode(actions: ActionInContext[], languageGenerator: LanguageGenerator, options: LanguageGeneratorOptions) { + const header = languageGenerator.generateHeader(options); + const footer = languageGenerator.generateFooter(options.saveStorage); + const actionTexts = actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); + const text = [header, ...actionTexts, footer].join('\n'); + return { header, footer, actionTexts, text }; +} + +export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions { + // Filter out all the properties from the device descriptor. + const cleanedOptions: Record = {}; + for (const property in options) { + if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property])) + cleanedOptions[property] = (options as any)[property]; + } + return cleanedOptions; +} + +export function toSignalMap(action: actions.Action) { + let popup: actions.PopupSignal | undefined; + let download: actions.DownloadSignal | undefined; + let dialog: actions.DialogSignal | undefined; + for (const signal of action.signals) { + if (signal.name === 'popup') + popup = signal; + else if (signal.name === 'download') + download = signal; + else if (signal.name === 'dialog') + dialog = signal; + } + return { + popup, + download, + dialog, + }; +} + +export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] { + const result: types.SmartKeyboardModifier[] = []; + if (modifiers & 1) + result.push('Alt'); + if (modifiers & 2) + result.push('ControlOrMeta'); + if (modifiers & 4) + result.push('ControlOrMeta'); + if (modifiers & 8) + result.push('Shift'); + return result; +} + +export function toClickOptions(action: actions.ClickAction): types.MouseClickOptions { + const modifiers = toKeyboardModifiers(action.modifiers); + const options: types.MouseClickOptions = {}; + if (action.button !== 'left') + options.button = action.button; + if (modifiers.length) + options.modifiers = modifiers; + if (action.clickCount > 2) + options.clickCount = action.clickCount; + if (action.position) + options.position = action.position; + return options; +} diff --git a/packages/playwright-core/src/server/codegen/languages.ts b/packages/playwright-core/src/server/codegen/languages.ts new file mode 100644 index 0000000000..d379be6be7 --- /dev/null +++ b/packages/playwright-core/src/server/codegen/languages.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JavaLanguageGenerator } from './java'; +import { JavaScriptLanguageGenerator } from './javascript'; +import { JsonlLanguageGenerator } from './jsonl'; +import { CSharpLanguageGenerator } from './csharp'; +import { PythonLanguageGenerator } from './python'; + +export function languageSet() { + return new Set([ + new JavaLanguageGenerator('junit'), + new JavaLanguageGenerator('library'), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), + new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), + new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), + new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), + new CSharpLanguageGenerator('mstest'), + new CSharpLanguageGenerator('nunit'), + new CSharpLanguageGenerator('library'), + new JsonlLanguageGenerator(), + ]); +} diff --git a/packages/playwright-core/src/server/recorder/python.ts b/packages/playwright-core/src/server/codegen/python.ts similarity index 89% rename from packages/playwright-core/src/server/recorder/python.ts rename to packages/playwright-core/src/server/codegen/python.ts index d393fb38e4..6ed101bcf0 100644 --- a/packages/playwright-core/src/server/recorder/python.ts +++ b/packages/playwright-core/src/server/codegen/python.ts @@ -14,13 +14,9 @@ * limitations under the License. */ -import type { BrowserContextOptions } from '../../..'; -import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language'; -import { sanitizeDeviceOptions, toSignalMap } from './language'; -import type { ActionInContext } from './codeGenerator'; -import type { Action } from './recorderActions'; -import type { MouseClickOptions } from './utils'; -import { toModifiers } from './utils'; +import type { BrowserContextOptions } from '../../../types/types'; +import type { ActionInContext, Language, LanguageGenerator, LanguageGeneratorOptions } from './types'; +import { sanitizeDeviceOptions, toSignalMap, toKeyboardModifiers, toClickOptions } from './language'; import { escapeWithQuotes, toSnakeCase, asLocator } from '../../utils'; import { deviceDescriptors } from '../deviceDescriptors'; @@ -59,20 +55,14 @@ export class PythonLanguageGenerator implements LanguageGenerator { return formatter.format(); } - let subject: string; - if (actionInContext.frame.isMainFrame) { - subject = pageAlias; - } else { - const locators = actionInContext.frame.selectorsChain.map(selector => `.frame_locator(${quote(selector)})`); - subject = `${pageAlias}${locators.join('')}`; - } - + const locators = actionInContext.frame.framePath.map(selector => `.${this._asLocator(selector)}.content_frame()`); + const subject = `${pageAlias}${locators.join('')}`; const signals = toSignalMap(action); if (signals.dialog) formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`); - let code = `${this._awaitPrefix}${this._generateActionCall(subject, action)}`; + let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`; if (signals.popup) { code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info { @@ -93,7 +83,8 @@ export class PythonLanguageGenerator implements LanguageGenerator { return formatter.format(); } - private _generateActionCall(subject: string, action: Action): string { + private _generateActionCall(subject: string, actionInContext: ActionInContext): string { + const action = actionInContext.action; switch (action.name) { case 'openPage': throw Error('Not reached'); @@ -103,16 +94,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { let method = 'click'; if (action.clickCount === 2) method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; + const options = toClickOptions(action); const optionsString = formatOptions(options, false); return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`; } @@ -125,7 +107,7 @@ export class PythonLanguageGenerator implements LanguageGenerator { case 'setInputFiles': return `${subject}.${this._asLocator(action.selector)}.set_input_files(${formatValue(action.files.length === 1 ? action.files[0] : action.files)})`; case 'press': { - const modifiers = toModifiers(action.modifiers); + const modifiers = toKeyboardModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); return `${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)})`; } diff --git a/packages/playwright-core/src/server/codegen/types.ts b/packages/playwright-core/src/server/codegen/types.ts new file mode 100644 index 0000000000..96f2aa85d1 --- /dev/null +++ b/packages/playwright-core/src/server/codegen/types.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { BrowserContextOptions, LaunchOptions } from '../../../types/types'; +import type * as actions from '../recorder/recorderActions'; +import type { Language } from '../../utils'; +export type { Language } from '../../utils'; + +export type LanguageGeneratorOptions = { + browserName: string; + launchOptions: LaunchOptions; + contextOptions: BrowserContextOptions; + deviceName?: string; + saveStorage?: string; +}; + +export type FrameDescription = { + pageAlias: string; + framePath: string[]; +}; + +export type ActionInContext = { + frame: FrameDescription; + description?: string; + action: actions.Action; + committed?: boolean; +}; + +export interface LanguageGenerator { + id: string; + groupName: string; + name: string; + highlighter: Language; + generateHeader(options: LanguageGeneratorOptions): string; + generateAction(actionInContext: ActionInContext): string; + generateFooter(saveStorage: string | undefined): string; +} diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 1d87485571..2a950d7c6a 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -52,7 +52,6 @@ export class DebugController extends SdkObject { initialize(codegenId: string, sdkLanguage: Language) { this._codegenId = codegenId; this._sdkLanguage = sdkLanguage; - Recorder.setAppFactory(async () => new InspectingRecorderApp(this)); } setAutoCloseAllowed(allowed: boolean) { @@ -62,7 +61,6 @@ export class DebugController extends SdkObject { dispose() { this.setReportStateChanged(false); this.setAutoCloseAllowed(false); - Recorder.setAppFactory(undefined); } setReportStateChanged(enabled: boolean) { @@ -199,7 +197,7 @@ export class DebugController extends SdkObject { const contexts = new Set(); for (const page of this._playwright.allPages()) contexts.add(page.context()); - const result = await Promise.all([...contexts].map(c => Recorder.show(c, { omitCallTracking: true }))); + const result = await Promise.all([...contexts].map(c => Recorder.show(c, () => Promise.resolve(new InspectingRecorderApp(this)), { omitCallTracking: true }))); return result.filter(Boolean) as Recorder[]; } diff --git a/packages/playwright-core/src/server/deviceDescriptorsSource.json b/packages/playwright-core/src/server/deviceDescriptorsSource.json index c25989baf5..c85866792a 100644 --- a/packages/playwright-core/src/server/deviceDescriptorsSource.json +++ b/packages/playwright-core/src/server/deviceDescriptorsSource.json @@ -110,7 +110,7 @@ "defaultBrowserType": "webkit" }, "Galaxy S5": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -121,7 +121,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -132,7 +132,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 740 @@ -143,7 +143,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S8 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; SM-G950U Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 740, "height": 360 @@ -154,7 +154,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 320, "height": 658 @@ -165,7 +165,7 @@ "defaultBrowserType": "chromium" }, "Galaxy S9+ landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G965U Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 658, "height": 320 @@ -176,7 +176,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 712, "height": 1138 @@ -187,7 +187,7 @@ "defaultBrowserType": "chromium" }, "Galaxy Tab S4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.1.0; SM-T837A) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 1138, "height": 712 @@ -1098,7 +1098,7 @@ "defaultBrowserType": "webkit" }, "LG Optimus L70": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1109,7 +1109,7 @@ "defaultBrowserType": "chromium" }, "LG Optimus L70 landscape": { - "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1120,7 +1120,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1131,7 +1131,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 550 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 550) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1142,7 +1142,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 360, "height": 640 @@ -1153,7 +1153,7 @@ "defaultBrowserType": "chromium" }, "Microsoft Lumia 950 landscape": { - "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36 Edge/14.14263", + "userAgent": "Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36 Edge/14.14263", "viewport": { "width": 640, "height": 360 @@ -1164,7 +1164,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 800, "height": 1280 @@ -1175,7 +1175,7 @@ "defaultBrowserType": "chromium" }, "Nexus 10 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 10 Build/MOB31T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 1280, "height": 800 @@ -1186,7 +1186,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 384, "height": 640 @@ -1197,7 +1197,7 @@ "defaultBrowserType": "chromium" }, "Nexus 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 384 @@ -1208,7 +1208,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1219,7 +1219,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1230,7 +1230,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1241,7 +1241,7 @@ "defaultBrowserType": "chromium" }, "Nexus 5X landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 5X Build/OPR4.170623.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1252,7 +1252,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1263,7 +1263,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.1.1; Nexus 6 Build/N6F26U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1274,7 +1274,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 412, "height": 732 @@ -1285,7 +1285,7 @@ "defaultBrowserType": "chromium" }, "Nexus 6P landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Nexus 6P Build/OPP3.170518.006) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 732, "height": 412 @@ -1296,7 +1296,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 600, "height": 960 @@ -1307,7 +1307,7 @@ "defaultBrowserType": "chromium" }, "Nexus 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 7 Build/MOB30X) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "viewport": { "width": 960, "height": 600 @@ -1362,7 +1362,7 @@ "defaultBrowserType": "webkit" }, "Pixel 2": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 411, "height": 731 @@ -1373,7 +1373,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 731, "height": 411 @@ -1384,7 +1384,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 411, "height": 823 @@ -1395,7 +1395,7 @@ "defaultBrowserType": "chromium" }, "Pixel 2 XL landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 8.0.0; Pixel 2 XL Build/OPD1.170816.004) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 823, "height": 411 @@ -1406,7 +1406,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 393, "height": 786 @@ -1417,7 +1417,7 @@ "defaultBrowserType": "chromium" }, "Pixel 3 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PQ1A.181105.017.A1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 786, "height": 393 @@ -1428,7 +1428,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 353, "height": 745 @@ -1439,7 +1439,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 10; Pixel 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 745, "height": 353 @@ -1450,7 +1450,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G)": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 412, "height": 892 @@ -1465,7 +1465,7 @@ "defaultBrowserType": "chromium" }, "Pixel 4a (5G) landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 4a (5G)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "height": 892, "width": 412 @@ -1480,7 +1480,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 393, "height": 851 @@ -1495,7 +1495,7 @@ "defaultBrowserType": "chromium" }, "Pixel 5 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 851, "height": 393 @@ -1510,7 +1510,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 412, "height": 915 @@ -1525,7 +1525,7 @@ "defaultBrowserType": "chromium" }, "Pixel 7 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "screen": { "width": 915, "height": 412 @@ -1540,7 +1540,7 @@ "defaultBrowserType": "chromium" }, "Moto G4": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 360, "height": 640 @@ -1551,7 +1551,7 @@ "defaultBrowserType": "chromium" }, "Moto G4 landscape": { - "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Mobile Safari/537.36", + "userAgent": "Mozilla/5.0 (Linux; Android 7.0; Moto G (4)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Mobile Safari/537.36", "viewport": { "width": 640, "height": 360 @@ -1562,7 +1562,7 @@ "defaultBrowserType": "chromium" }, "Desktop Chrome HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "screen": { "width": 1792, "height": 1120 @@ -1577,7 +1577,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge HiDPI": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22", "screen": { "width": 1792, "height": 1120 @@ -1622,7 +1622,7 @@ "defaultBrowserType": "webkit" }, "Desktop Chrome": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36", "screen": { "width": 1920, "height": 1080 @@ -1637,7 +1637,7 @@ "defaultBrowserType": "chromium" }, "Desktop Edge": { - "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.36 Safari/537.36 Edg/128.0.6613.36", + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.22 Safari/537.36 Edg/129.0.6668.22", "screen": { "width": 1920, "height": 1080 diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index 5654950360..c70d8e825a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -39,6 +39,7 @@ import type { Dialog } from '../dialog'; import type { ConsoleMessage } from '../console'; import { serializeError } from '../errors'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; +import { RecorderApp } from '../recorder/recorderApp'; export class BrowserContextDispatcher extends Dispatcher implements channels.BrowserContextChannel { _type_EventTarget = true; @@ -291,7 +292,7 @@ export class BrowserContextDispatcher extends Dispatcher { - await Recorder.show(this._context, params); + await Recorder.show(this._context, RecorderApp.factory(this._context), params); } async pause(params: channels.BrowserContextPauseParams, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 60aa899412..175d2a0f4b 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -536,7 +536,7 @@ export class ElementHandle extends js.JSHandle { return this._retryPointerAction(progress, 'tap', true /* waitForEnabled */, point => this._page.touchscreen.tap(point.x, point.y), { ...options, waitAfter: 'disabled' }); } - async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise { + async selectOption(metadata: CallMetadata, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { const result = await this._selectOption(progress, elements, values, options); @@ -544,7 +544,7 @@ export class ElementHandle extends js.JSHandle { }, this._page._timeoutSettings.timeout(options)); } - async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions): Promise { + async _selectOption(progress: Progress, elements: ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions): Promise { let resultingOptions: string[] = []; await this._retryAction(progress, 'select option', async () => { await progress.beforeInputAction(this); diff --git a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts index 266f5bcb83..978eb30bd4 100644 --- a/packages/playwright-core/src/server/firefox/ffNetworkManager.ts +++ b/packages/playwright-core/src/server/firefox/ffNetworkManager.ts @@ -226,7 +226,7 @@ class FFRouteImpl implements network.RouteDelegate { this._request = request; } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { + async continue(overrides: types.NormalizedContinueOverrides) { await this._session.sendMayFail('Network.resumeInterceptedRequest', { requestId: this._request._id, url: overrides.url, diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 931ba8ef73..3b952ea02a 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -291,8 +291,7 @@ export class FrameManager { if (request._documentId) frame.setPendingDocument({ documentId: request._documentId, request }); if (request._isFavicon) { - if (route) - route.continue(request, { isFallback: true }).catch(() => {}); + route?.continue({ isFallback: true }).catch(() => {}); return; } this._page.emitOnContext(BrowserContext.Events.Request, request); @@ -800,7 +799,7 @@ export class Frame extends SdkObject { const result = await resolved.injected.evaluateHandle((injected, { info, root }) => { const elements = injected.querySelectorAll(info.parsed, root || document); const element: Element | undefined = elements[0]; - const visible = element ? injected.isVisible(element) : false; + const visible = element ? injected.utils.isElementVisible(element) : false; let log = ''; if (elements.length > 1) { if (info.strict) @@ -1344,7 +1343,7 @@ export class Frame extends SdkObject { }, this._page._timeoutSettings.timeout(options)); } - async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: { noWaitAfter?: boolean } & types.CommonActionOptions = {}): Promise { + async selectOption(metadata: CallMetadata, selector: string, elements: dom.ElementHandle[], values: types.SelectOption[], options: types.CommonActionOptions = {}): Promise { const controller = new ProgressController(metadata, this); return controller.run(async progress => { return await this._retryWithProgressIfNotConnected(progress, selector, options.strict, !options.force /* performLocatorHandlersCheckpoint */, handle => handle._selectOption(progress, elements, values, options)); diff --git a/packages/playwright-core/src/server/injected/.eslintrc.js b/packages/playwright-core/src/server/injected/.eslintrc.js index e96e2a9f80..eccd5b787d 100644 --- a/packages/playwright-core/src/server/injected/.eslintrc.js +++ b/packages/playwright-core/src/server/injected/.eslintrc.js @@ -1,10 +1,21 @@ +const path = require('path'); + module.exports = { - rules: { - "no-restricted-globals": [ - "error", - { "name": "window" }, - { "name": "document" }, - { "name": "globalThis" }, - ] - } + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint", "notice"], + parserOptions: { + ecmaVersion: 9, + sourceType: "module", + project: path.join(__dirname, '../../../../../tsconfig.json'), + }, + rules: { + "no-restricted-globals": [ + "error", + { "name": "window" }, + { "name": "document" }, + { "name": "globalThis" }, + ], + '@typescript-eslint/no-floating-promises': 'error', + "@typescript-eslint/no-unnecessary-boolean-literal-compare": 2, + }, }; diff --git a/packages/playwright-core/src/server/injected/clock.ts b/packages/playwright-core/src/server/injected/clock.ts index 48cc9276a2..b2daf190f3 100644 --- a/packages/playwright-core/src/server/injected/clock.ts +++ b/packages/playwright-core/src/server/injected/clock.ts @@ -216,7 +216,7 @@ export class ClockController { const sinceLastSync = now - this._realTime!.lastSyncTicks; this._realTime!.lastSyncTicks = now; // eslint-disable-next-line no-console - this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); + void this._runTo(shiftTicks(this._now.ticks, sinceLastSync)).catch(e => console.error(e)).then(() => this._updateRealTimeTimer()); }, callAt - this._now.ticks), }; } @@ -239,7 +239,12 @@ export class ClockController { addTimer(options: { func: TimerHandler, type: TimerType, delay?: number | string, args?: any[] }): number { this._replayLogOnce(); - if (options.func === undefined) + + if (options.type === TimerType.AnimationFrame && !options.func) + throw new Error('Callback must be provided to requestAnimationFrame calls'); + if (options.type === TimerType.IdleCallback && !options.func) + throw new Error('Callback must be provided to requestIdleCallback calls'); + if ([TimerType.Timeout, TimerType.Interval].includes(options.type) && !options.func && options.delay === undefined) throw new Error('Callback must be provided to timer calls'); let delay = options.delay ? +options.delay : 0; diff --git a/packages/playwright-core/src/server/injected/injectedScript.ts b/packages/playwright-core/src/server/injected/injectedScript.ts index 2323648bae..69fe959f81 100644 --- a/packages/playwright-core/src/server/injected/injectedScript.ts +++ b/packages/playwright-core/src/server/injected/injectedScript.ts @@ -29,11 +29,13 @@ import type { CSSComplexSelectorList } from '../../utils/isomorphic/cssParser'; import { generateSelector, type GenerateSelectorOptions } from './selectorGenerator'; import type * as channels from '@protocol/channels'; import { Highlight } from './highlight'; -import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription } from './roleUtils'; +import { getChecked, getAriaDisabled, getAriaRole, getElementAccessibleName, getElementAccessibleDescription, beginAriaCaches, endAriaCaches } from './roleUtils'; import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils'; import { asLocator } from '../../utils/isomorphic/locatorGenerators'; import type { Language } from '../../utils/isomorphic/locatorGenerators'; -import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; +import { cacheNormalizedWhitespaces, escapeHTML, escapeHTMLAttribute, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils'; +import { selectorForSimpleDomNodeId, generateSimpleDomNode } from './simpleDom'; +import type { SimpleDomNode } from './simpleDom'; export type FrameExpectParams = Omit & { expectedValue?: any }; @@ -66,7 +68,25 @@ export class InjectedScript { // eslint-disable-next-line no-restricted-globals readonly window: Window & typeof globalThis; readonly document: Document; - readonly utils = { isInsideScope, elementText, asLocator, normalizeWhiteSpace, cacheNormalizedWhitespaces }; + + // Recorder must use any external dependencies through InjectedScript. + // Otherwise it will end up with a copy of all modules it uses, and any + // module-level globals will be duplicated, which leads to subtle bugs. + readonly utils = { + asLocator, + beginAriaCaches, + cacheNormalizedWhitespaces, + elementText, + endAriaCaches, + escapeHTML, + escapeHTMLAttribute, + getAriaRole, + getElementAccessibleDescription, + getElementAccessibleName, + isElementVisible, + isInsideScope, + normalizeWhiteSpace, + }; // eslint-disable-next-line no-restricted-globals constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) { @@ -426,10 +446,6 @@ export class InjectedScript { return new constrFunction(this, params); } - isVisible(element: Element): boolean { - return isElementVisible(element); - } - async viewportRatio(element: Element): Promise { return await new Promise(resolve => { const observer = new IntersectionObserver(entries => { @@ -567,9 +583,9 @@ export class InjectedScript { } if (state === 'visible') - return this.isVisible(element); + return isElementVisible(element); if (state === 'hidden') - return !this.isVisible(element); + return !isElementVisible(element); const disabled = getAriaDisabled(element); if (state === 'disabled') @@ -1297,16 +1313,15 @@ export class InjectedScript { throw this.createStacklessError('Unknown expect matcher: ' + expression); } - getElementAccessibleName(element: Element, includeHidden?: boolean): string { - return getElementAccessibleName(element, !!includeHidden); + generateSimpleDomNode(selector: string): SimpleDomNode | undefined { + const element = this.querySelector(this.parseSelector(selector), this.document.documentElement, true); + if (!element) + return; + return generateSimpleDomNode(this, element); } - getElementAccessibleDescription(element: Element, includeHidden?: boolean): string { - return getElementAccessibleDescription(element, !!includeHidden); - } - - getAriaRole(element: Element) { - return getAriaRole(element); + selectorForSimpleDomNodeId(nodeId: string) { + return selectorForSimpleDomNodeId(this, nodeId); } } diff --git a/packages/playwright-core/src/server/injected/recorder/DEPS.list b/packages/playwright-core/src/server/injected/recorder/DEPS.list index ee39467fea..1f58b3d5d0 100644 --- a/packages/playwright-core/src/server/injected/recorder/DEPS.list +++ b/packages/playwright-core/src/server/injected/recorder/DEPS.list @@ -1,4 +1,4 @@ -# Recorder must use any external dependencies through InjectedScript. +# Recorder must use any external dependencies through injectedScript.utils. # Otherwise it will end up with a copy of all modules it uses, and any # module-level globals will be duplicated, which leads to subtle bugs. [*] diff --git a/packages/playwright-core/src/server/injected/recorder/recorder.ts b/packages/playwright-core/src/server/injected/recorder/recorder.ts index 3432f159dd..8cbf11964f 100644 --- a/packages/playwright-core/src/server/injected/recorder/recorder.ts +++ b/packages/playwright-core/src/server/injected/recorder/recorder.ts @@ -21,9 +21,10 @@ import type { Mode, OverlayState, UIState } from '@recorder/recorderTypes'; import type { ElementText } from '../selectorUtils'; import type { Highlight, HighlightOptions } from '../highlight'; import clipPaths from './clipPaths'; +import type { SimpleDomNode } from '../simpleDom'; interface RecorderDelegate { - performAction?(action: actions.Action): Promise; + performAction?(action: actions.PerformOnRecordAction): Promise; recordAction?(action: actions.Action): Promise; setSelector?(selector: string): Promise; setMode?(mode: Mode): Promise; @@ -168,7 +169,7 @@ class InspectTool implements RecorderTool { if (this._hoveredModel?.tooltipListItemSelected) this._reset(true); else if (this._assertVisibility) - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); } } @@ -182,15 +183,15 @@ class InspectTool implements RecorderTool { private _commit(selector: string) { if (this._assertVisibility) { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'assertVisible', selector, signals: [], }); - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); this._recorder.overlay?.flashToolSucceeded('assertingVisibility'); } else { - this._recorder.delegate.setSelector?.(selector); + this._recorder.setSelector(selector); } } @@ -338,7 +339,7 @@ class RecordActionTool implements RecorderTool { const target = this._recorder.deepEventTarget(event); if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'setInputFiles', selector: this._activeModel!.selector, signals: [], @@ -348,7 +349,7 @@ class RecordActionTool implements RecorderTool { } if (isRangeInput(target)) { - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'fill', // must use hoveredModel instead of activeModel for it to work in webkit selector: this._hoveredModel!.selector, @@ -367,7 +368,7 @@ class RecordActionTool implements RecorderTool { // Non-navigating actions are simply recorded by Playwright. if (this._consumedDueWrongTarget(event)) return; - this._recorder.delegate.recordAction?.({ + this._recorder.recordAction({ name: 'fill', selector: this._activeModel!.selector, signals: [], @@ -483,26 +484,27 @@ class RecordActionTool implements RecorderTool { return true; } - private async _performAction(action: actions.Action) { + private _performAction(action: actions.PerformOnRecordAction) { this._hoveredElement = null; this._hoveredModel = null; this._activeModel = null; this._recorder.updateHighlight(null, false); this._performingAction = true; - await this._recorder.delegate.performAction?.(action).catch(() => {}); - this._performingAction = false; + void this._recorder.performAction(action).then(() => { + this._performingAction = false; - // If that was a keyboard action, it similarly requires new selectors for active model. - this._onFocus(false); + // If that was a keyboard action, it similarly requires new selectors for active model. + this._onFocus(false); - if (this._recorder.injectedScript.isUnderTest) { - // Serialize all to string as we cannot attribute console message to isolated world - // in Firefox. - console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console - hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null, - active: this._activeModel ? (this._activeModel as any).selector : null, - })); - } + if (this._recorder.injectedScript.isUnderTest) { + // Serialize all to string as we cannot attribute console message to isolated world + // in Firefox. + console.error('Action performed for test: ' + JSON.stringify({ // eslint-disable-line no-console + hovered: this._hoveredModel ? (this._hoveredModel as any).selector : null, + active: this._activeModel ? (this._activeModel as any).selector : null, + })); + } + }); } private _shouldGenerateKeyPressFor(event: KeyboardEvent): boolean { @@ -613,7 +615,7 @@ class TextAssertionTool implements RecorderTool { onKeyDown(event: KeyboardEvent) { if (event.key === 'Escape') - this._recorder.delegate.setMode?.('recording'); + this._recorder.setMode('recording'); consumeEvent(event); } @@ -680,8 +682,8 @@ class TextAssertionTool implements RecorderTool { if (!this._action || !this._dialog.isShowing()) return; this._dialog.close(); - this._recorder.delegate.recordAction?.(this._action); - this._recorder.delegate.setMode?.('recording'); + this._recorder.recordAction(this._action); + this._recorder.setMode('recording'); } private _showDialog() { @@ -726,8 +728,8 @@ class TextAssertionTool implements RecorderTool { const action = this._generateAction(); if (!action) return; - this._recorder.delegate.recordAction?.(action); - this._recorder.delegate.setMode?.('recording'); + this._recorder.recordAction(action); + this._recorder.setMode('recording'); this._recorder.overlay?.flashToolSucceeded('assertingValue'); } } @@ -799,7 +801,7 @@ class Overlay { this._dragState = { offsetX: this._offsetX, dragStart: { x: (event as MouseEvent).clientX, y: 0 } }; }), addEventListener(this._recordToggle, 'click', () => { - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby'); + this._recorder.setMode(this._recorder.state.mode === 'none' || this._recorder.state.mode === 'standby' || this._recorder.state.mode === 'inspecting' ? 'recording' : 'standby'); }), addEventListener(this._pickLocatorToggle, 'click', () => { const newMode: Record = { @@ -812,19 +814,19 @@ class Overlay { 'assertingVisibility': 'recording-inspecting', 'assertingValue': 'recording-inspecting', }; - this._recorder.delegate.setMode?.(newMode[this._recorder.state.mode]); + this._recorder.setMode(newMode[this._recorder.state.mode]); }), addEventListener(this._assertVisibilityToggle, 'click', () => { if (!this._assertVisibilityToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility'); + this._recorder.setMode(this._recorder.state.mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility'); }), addEventListener(this._assertTextToggle, 'click', () => { if (!this._assertTextToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); + this._recorder.setMode(this._recorder.state.mode === 'assertingText' ? 'recording' : 'assertingText'); }), addEventListener(this._assertValuesToggle, 'click', () => { if (!this._assertValuesToggle.classList.contains('disabled')) - this._recorder.delegate.setMode?.(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); + this._recorder.setMode(this._recorder.state.mode === 'assertingValue' ? 'recording' : 'assertingValue'); }), ]; } @@ -890,7 +892,7 @@ class Overlay { const halfGapSize = (this._recorder.injectedScript.window.innerWidth - this._measure.width) / 2 - 10; this._offsetX = Math.max(-halfGapSize, Math.min(halfGapSize, this._offsetX)); this._updateVisualPosition(); - this._recorder.delegate.setOverlayState?.({ offsetX: this._offsetX }); + this._recorder.setOverlayState({ offsetX: this._offsetX }); consumeEvent(event); return true; } @@ -924,9 +926,14 @@ export class Recorder { readonly highlight: Highlight; readonly overlay: Overlay | undefined; private _stylesheet: CSSStyleSheet; - state: UIState = { mode: 'none', testIdAttributeName: 'data-testid', language: 'javascript', overlay: { offsetX: 0 } }; + state: UIState = { + mode: 'none', + testIdAttributeName: 'data-testid', + language: 'javascript', + overlay: { offsetX: 0 }, + }; readonly document: Document; - delegate: RecorderDelegate = {}; + private _delegate: RecorderDelegate = {}; constructor(injectedScript: InjectedScript) { this.document = injectedScript.document; @@ -994,7 +1001,7 @@ export class Recorder { } setUIState(state: UIState, delegate: RecorderDelegate) { - this.delegate = delegate; + this._delegate = delegate; if (state.actionPoint && this.state.actionPoint && state.actionPoint.x === this.state.actionPoint.x && state.actionPoint.y === this.state.actionPoint.y) { // All good. @@ -1155,7 +1162,7 @@ export class Recorder { tooltipText = this.injectedScript.utils.asLocator(this.state.language, model.selector); this.highlight.updateHighlight(model?.elements || [], { ...model, tooltipText }); if (userGesture) - this.delegate.highlightUpdated?.(); + this._delegate.highlightUpdated?.(); } private _ignoreOverlayEvent(event: Event) { @@ -1172,6 +1179,26 @@ export class Recorder { } return event.composedPath()[0] as HTMLElement; } + + setMode(mode: Mode) { + void this._delegate.setMode?.(mode); + } + + async performAction(action: actions.PerformOnRecordAction) { + await this._delegate.performAction?.(action).catch(() => {}); + } + + recordAction(action: actions.Action) { + void this._delegate.recordAction?.(action); + } + + setOverlayState(state: { offsetX: number; }) { + void this._delegate.setOverlayState?.(state); + } + + setSelector(selector: string) { + void this._delegate.setSelector?.(selector); + } } class Dialog { @@ -1361,8 +1388,8 @@ function createSvgElement(doc: Document, { tagName, attrs, children }: SvgJson): } interface Embedder { - __pw_recorderPerformAction(action: actions.Action): Promise; - __pw_recorderRecordAction(action: actions.Action): Promise; + __pw_recorderPerformAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode): Promise; + __pw_recorderRecordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise; __pw_recorderState(): Promise; __pw_recorderSetSelector(selector: string): Promise; __pw_recorderSetMode(mode: Mode): Promise; @@ -1407,12 +1434,12 @@ export class PollingRecorder implements RecorderDelegate { this._pollRecorderModeTimer = this._recorder.injectedScript.builtinSetTimeout(() => this._pollRecorderMode(), pollPeriod); } - async performAction(action: actions.Action) { - await this._embedder.__pw_recorderPerformAction(action); + async performAction(action: actions.PerformOnRecordAction, simpleDomNode?: SimpleDomNode) { + await this._embedder.__pw_recorderPerformAction(action, simpleDomNode); } - async recordAction(action: actions.Action): Promise { - await this._embedder.__pw_recorderRecordAction(action); + async recordAction(action: actions.Action, simpleDomNode?: SimpleDomNode): Promise { + await this._embedder.__pw_recorderRecordAction(action, simpleDomNode); } async setSelector(selector: string): Promise { diff --git a/packages/playwright-core/src/server/injected/simpleDom.ts b/packages/playwright-core/src/server/injected/simpleDom.ts new file mode 100644 index 0000000000..c31862cd6c --- /dev/null +++ b/packages/playwright-core/src/server/injected/simpleDom.ts @@ -0,0 +1,120 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { InjectedScript } from './injectedScript'; + +const leafRoles = new Set([ + 'button', + 'checkbox', + 'combobox', + 'link', + 'textbox', +]); + +export type SimpleDom = { + markup: string; + elements: Map; +}; + +export type SimpleDomNode = { + dom: SimpleDom; + id: string; + tag: string; +}; + +let lastDom: SimpleDom | undefined; + +export function generateSimpleDom(injectedScript: InjectedScript): SimpleDom { + return generate(injectedScript).dom; +} + +export function generateSimpleDomNode(injectedScript: InjectedScript, target: Element): SimpleDomNode { + return generate(injectedScript, target).node!; +} + +export function selectorForSimpleDomNodeId(injectedScript: InjectedScript, id: string): string { + const element = lastDom?.elements.get(id); + if (!element) + throw new Error(`Internal error: element with id "${id}" not found`); + return injectedScript.generateSelectorSimple(element); +} + +function generate(injectedScript: InjectedScript, target?: Element): { dom: SimpleDom, node?: SimpleDomNode } { + const normalizeWhitespace = (text: string) => text.replace(/[\s\n]+/g, match => match.includes('\n') ? '\n' : ' '); + const tokens: string[] = []; + const elements = new Map(); + let lastId = 0; + let resultTarget: { tag: string, id: string } | undefined; + const visit = (node: Node) => { + if (node.nodeType === Node.TEXT_NODE) { + tokens.push(node.nodeValue!); + return; + } + + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + if (element.nodeName === 'SCRIPT' || element.nodeName === 'STYLE' || element.nodeName === 'NOSCRIPT') + return; + if (injectedScript.utils.isElementVisible(element)) { + const role = injectedScript.utils.getAriaRole(element) as string; + if (role && leafRoles.has(role)) { + let value: string | undefined; + if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') + value = (element as HTMLInputElement | HTMLTextAreaElement).value; + const name = injectedScript.utils.getElementAccessibleName(element, false); + const structuralId = String(++lastId); + elements.set(structuralId, element); + tokens.push(renderTag(injectedScript, role, name, structuralId, { value })); + if (element === target) { + const tagNoValue = renderTag(injectedScript, role, name, structuralId); + resultTarget = { tag: tagNoValue, id: structuralId }; + } + return; + } + } + for (let child = element.firstChild; child; child = child.nextSibling) + visit(child); + } + }; + injectedScript.utils.beginAriaCaches(); + try { + visit(injectedScript.document.body); + } finally { + injectedScript.utils.endAriaCaches(); + } + const dom = { + markup: normalizeWhitespace(tokens.join(' ')), + elements + }; + + if (target && !resultTarget) + throw new Error('Target element is not in the simple DOM'); + + lastDom = dom; + + return { dom, node: resultTarget ? { dom, ...resultTarget } : undefined }; +} + +function renderTag(injectedScript: InjectedScript, role: string, name: string, id: string, params?: { value?: string }): string { + const escapedTextContent = injectedScript.utils.escapeHTML(name); + const escapedValue = injectedScript.utils.escapeHTMLAttribute(params?.value || ''); + switch (role) { + case 'button': return ``; + case 'link': return `${escapedTextContent}`; + case 'textbox': return ``; + } + return `
${escapedTextContent}
`; +} diff --git a/packages/playwright-core/src/server/network.ts b/packages/playwright-core/src/server/network.ts index fd62e1751b..e18b43708d 100644 --- a/packages/playwright-core/src/server/network.ts +++ b/packages/playwright-core/src/server/network.ts @@ -324,7 +324,7 @@ export class Route extends SdkObject { this._request._setOverrides(overrides); if (!overrides.isFallback) this._request._context.emit(BrowserContext.Events.RequestContinued, this._request); - await this._delegate.continue(this._request, overrides); + await this._delegate.continue(overrides); this._endHandling(); } @@ -612,7 +612,7 @@ export class WebSocket extends SdkObject { export interface RouteDelegate { abort(errorCode: string): Promise; fulfill(response: types.NormalizedFulfillResponse): Promise; - continue(request: Request, overrides: types.NormalizedContinueOverrides): Promise; + continue(overrides: types.NormalizedContinueOverrides): Promise; } // List taken from https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml with extra 306 and 418 codes. diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 93f706581f..97316c2f9e 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -14,42 +14,25 @@ * limitations under the License. */ -import * as fs from 'fs'; -import type * as actions from './recorder/recorderActions'; import type * as channels from '@protocol/channels'; -import type { ActionInContext } from './recorder/codeGenerator'; -import { CodeGenerator } from './recorder/codeGenerator'; -import { toClickOptions, toModifiers } from './recorder/utils'; -import { Page } from './page'; -import { Frame } from './frames'; -import { BrowserContext } from './browserContext'; -import { JavaLanguageGenerator } from './recorder/java'; -import { JavaScriptLanguageGenerator } from './recorder/javascript'; -import { JsonlLanguageGenerator } from './recorder/jsonl'; -import { CSharpLanguageGenerator } from './recorder/csharp'; -import { PythonLanguageGenerator } from './recorder/python'; -import * as recorderSource from '../generated/recorderSource'; -import * as consoleApiSource from '../generated/consoleApiSource'; -import { EmptyRecorderApp } from './recorder/recorderApp'; -import type { IRecorderApp } from './recorder/recorderApp'; -import { RecorderApp } from './recorder/recorderApp'; -import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; -import type { Point } from '../common/types'; import type { CallLog, CallLogStatus, EventData, Mode, OverlayState, Source, UIState } from '@recorder/recorderTypes'; -import { createGuid, isUnderTest, monotonicTime } from '../utils'; -import { metadataToCallLog } from './recorder/recorderUtils'; -import { Debugger } from './debugger'; -import { EventEmitter } from 'events'; -import { raceAgainstDeadline } from '../utils/timeoutRunner'; -import type { Language, LanguageGenerator } from './recorder/language'; +import * as fs from 'fs'; +import type { Point } from '../common/types'; +import * as consoleApiSource from '../generated/consoleApiSource'; +import { isUnderTest } from '../utils'; import { locatorOrSelectorAsSelector } from '../utils/isomorphic/locatorParser'; -import { quoteCSSAttributeValue, eventsHelper, type RegisteredListener } from '../utils'; -import type { Dialog } from './dialog'; - -type BindingSource = { frame: Frame, page: Page }; +import { BrowserContext } from './browserContext'; +import { type Language } from './codegen/types'; +import { Debugger } from './debugger'; +import type { CallMetadata, InstrumentationListener, SdkObject } from './instrumentation'; +import { ContextRecorder, generateFrameSelector } from './recorder/contextRecorder'; +import { type IRecorderApp } from './recorder/recorderApp'; +import { buildFullSelector, metadataToCallLog } from './recorder/recorderUtils'; const recorderSymbol = Symbol('recorderSymbol'); +export type RecorderAppFactory = (recorder: Recorder) => Promise; + export class Recorder implements InstrumentationListener { private _context: BrowserContext; private _mode: Mode; @@ -61,40 +44,38 @@ export class Recorder implements InstrumentationListener { private _userSources = new Map(); private _debugger: Debugger; private _contextRecorder: ContextRecorder; - private _handleSIGINT: boolean | undefined; private _omitCallTracking = false; private _currentLanguage: Language; - private static recorderAppFactory: ((recorder: Recorder) => Promise) | undefined; - - static setAppFactory(recorderAppFactory: ((recorder: Recorder) => Promise) | undefined) { - Recorder.recorderAppFactory = recorderAppFactory; - } - - static showInspector(context: BrowserContext) { + static showInspector(context: BrowserContext, recorderAppFactory: RecorderAppFactory) { const params: channels.BrowserContextRecorderSupplementEnableParams = {}; if (isUnderTest()) params.language = process.env.TEST_INSPECTOR_LANGUAGE; - Recorder.show(context, params).catch(() => {}); + Recorder.show(context, recorderAppFactory, params).catch(() => {}); } - static show(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { + static show(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { let recorderPromise = (context as any)[recorderSymbol] as Promise; if (!recorderPromise) { - const recorder = new Recorder(context, params); - recorderPromise = recorder.install().then(() => recorder); + recorderPromise = Recorder._create(context, recorderAppFactory, params); (context as any)[recorderSymbol] = recorderPromise; } return recorderPromise; } + private static async _create(context: BrowserContext, recorderAppFactory: RecorderAppFactory, params: channels.BrowserContextRecorderSupplementEnableParams = {}): Promise { + const recorder = new Recorder(context, params); + const recorderApp = await recorderAppFactory(recorder); + await recorder._install(recorderApp); + return recorder; + } + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { this._mode = params.mode || 'none'; - this._contextRecorder = new ContextRecorder(context, params); + this._contextRecorder = new ContextRecorder(context, params, {}); this._context = context; this._omitCallTracking = !!params.omitCallTracking; this._debugger = context.debugger(); - this._handleSIGINT = params.handleSIGINT; context.instrumentation.addListener(this, context); this._currentLanguage = this._contextRecorder.languageName(); @@ -104,14 +85,7 @@ export class Recorder implements InstrumentationListener { } } - private static async defaultRecorderAppFactory(recorder: Recorder) { - if (process.env.PW_CODEGEN_NO_INSPECTOR) - return new EmptyRecorderApp(); - return await RecorderApp.open(recorder, recorder._context, recorder._handleSIGINT); - } - - async install() { - const recorderApp = await (Recorder.recorderAppFactory || Recorder.defaultRecorderAppFactory)(this); + private async _install(recorderApp: IRecorderApp) { this._recorderApp = recorderApp; recorderApp.once('close', () => { this._debugger.resume(false); @@ -158,7 +132,7 @@ export class Recorder implements InstrumentationListener { this._context.once(BrowserContext.Events.Close, () => { this._contextRecorder.dispose(); this._context.instrumentation.removeListener(this); - recorderApp.close().catch(() => {}); + this._recorderApp?.close().catch(() => {}); }); this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], primaryFileName: string }) => { this._recorderSources = data.sources; @@ -191,15 +165,8 @@ export class Recorder implements InstrumentationListener { }); await this._context.exposeBinding('__pw_recorderSetSelector', false, async ({ frame }, selector: string) => { - const selectorPromises: Promise[] = []; - let currentFrame: Frame | null = frame; - while (currentFrame) { - selectorPromises.push(findFrameSelector(currentFrame)); - currentFrame = currentFrame.parentFrame(); - } - const fullSelector = (await Promise.all(selectorPromises)).filter(Boolean); - fullSelector.push(selector); - await this._recorderApp?.setSelector(fullSelector.join(' >> internal:control=enter-frame >> '), true); + const selectorChain = await generateFrameSelector(frame); + await this._recorderApp?.setSelector(buildFullSelector(selectorChain, selector), true); }); await this._context.exposeBinding('__pw_recorderSetMode', false, async ({ frame }, mode: Mode) => { @@ -225,7 +192,7 @@ export class Recorder implements InstrumentationListener { this._pausedStateChanged(); this._debugger.on(Debugger.Events.PausedStateChanged, () => this._pausedStateChanged()); - (this._context as any).recorderAppForTest = recorderApp; + (this._context as any).recorderAppForTest = this._recorderApp; } _pausedStateChanged() { @@ -369,329 +336,8 @@ export class Recorder implements InstrumentationListener { } } -class ContextRecorder extends EventEmitter { - static Events = { - Change: 'change' - }; - - private _generator: CodeGenerator; - private _pageAliases = new Map(); - private _lastPopupOrdinal = 0; - private _lastDialogOrdinal = -1; - private _lastDownloadOrdinal = -1; - private _timers = new Set(); - private _context: BrowserContext; - private _params: channels.BrowserContextRecorderSupplementEnableParams; - private _recorderSources: Source[]; - private _throttledOutputFile: ThrottledFile | null = null; - private _orderedLanguages: LanguageGenerator[] = []; - private _listeners: RegisteredListener[] = []; - - constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { - super(); - this._context = context; - this._params = params; - this._recorderSources = []; - const language = params.language || context.attribution.playwright.options.sdkLanguage; - this.setOutput(language, params.outputFile); - const generator = new CodeGenerator(context._browser.options.name, params.mode === 'recording', params.launchOptions || {}, params.contextOptions || {}, params.device, params.saveStorage); - generator.on('change', () => { - this._recorderSources = []; - for (const languageGenerator of this._orderedLanguages) { - const { header, footer, actions, text } = generator.generateStructure(languageGenerator); - const source: Source = { - isRecorded: true, - label: languageGenerator.name, - group: languageGenerator.groupName, - id: languageGenerator.id, - text, - header, - footer, - actions, - language: languageGenerator.highlighter, - highlight: [] - }; - source.revealLine = text.split('\n').length - 1; - this._recorderSources.push(source); - if (languageGenerator === this._orderedLanguages[0]) - this._throttledOutputFile?.setContent(source.text); - } - this.emit(ContextRecorder.Events.Change, { - sources: this._recorderSources, - primaryFileName: this._orderedLanguages[0].id - }); - }); - context.on(BrowserContext.Events.BeforeClose, () => { - this._throttledOutputFile?.flush(); - }); - this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { - this._throttledOutputFile?.flush(); - })); - this._generator = generator; - } - - setOutput(codegenId: string, outputFile?: string) { - const languages = new Set([ - new JavaLanguageGenerator('junit'), - new JavaLanguageGenerator('library'), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), - new JavaScriptLanguageGenerator(/* isPlaywrightTest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */true), - new PythonLanguageGenerator(/* isAsync */false, /* isPytest */false), - new PythonLanguageGenerator(/* isAsync */true, /* isPytest */false), - new CSharpLanguageGenerator('mstest'), - new CSharpLanguageGenerator('nunit'), - new CSharpLanguageGenerator('library'), - new JsonlLanguageGenerator(), - ]); - const primaryLanguage = [...languages].find(l => l.id === codegenId); - if (!primaryLanguage) - throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); - languages.delete(primaryLanguage); - this._orderedLanguages = [primaryLanguage, ...languages]; - this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null; - this._generator?.restart(); - } - - languageName(id?: string): Language { - for (const lang of this._orderedLanguages) { - if (!id || lang.id === id) - return lang.highlighter; - } - return 'javascript'; - } - - async install() { - this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); - for (const page of this._context.pages()) - this._onPage(page); - this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); - - // Input actions that potentially lead to navigation are intercepted on the page and are - // performed by the Playwright. - await this._context.exposeBinding('__pw_recorderPerformAction', false, - (source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)); - - // Other non-essential actions are simply being recorded. - await this._context.exposeBinding('__pw_recorderRecordAction', false, - (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); - - await this._context.extendInjectedScript(recorderSource.source); - } - - setEnabled(enabled: boolean) { - this._generator.setEnabled(enabled); - } - - dispose() { - for (const timer of this._timers) - clearTimeout(timer); - this._timers.clear(); - eventsHelper.removeEventListeners(this._listeners); - } - - private async _onPage(page: Page) { - // First page is called page, others are called popup1, popup2, etc. - const frame = page.mainFrame(); - page.on('close', () => { - this._generator.addAction({ - frame: this._describeMainFrame(page), - committed: true, - action: { - name: 'closePage', - signals: [], - } - }); - this._pageAliases.delete(page); - }); - frame.on(Frame.Events.InternalNavigation, event => { - if (event.isPublic) - this._onFrameNavigated(frame, page); - }); - page.on(Page.Events.Download, () => this._onDownload(page)); - const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; - const pageAlias = 'page' + suffix; - this._pageAliases.set(page, pageAlias); - - if (page.opener()) { - this._onPopup(page.opener()!, page); - } else { - this._generator.addAction({ - frame: this._describeMainFrame(page), - committed: true, - action: { - name: 'openPage', - url: page.mainFrame().url(), - signals: [], - } - }); - } - } - - clearScript(): void { - this._generator.restart(); - if (this._params.mode === 'recording') { - for (const page of this._context.pages()) - this._onFrameNavigated(page.mainFrame(), page); - } - } - - private _describeMainFrame(page: Page): actions.FrameDescription { - return { - pageAlias: this._pageAliases.get(page)!, - isMainFrame: true, - }; - } - - private async _describeFrame(frame: Frame): Promise { - const page = frame._page; - const pageAlias = this._pageAliases.get(page)!; - const chain: Frame[] = []; - for (let ancestor: Frame | null = frame; ancestor; ancestor = ancestor.parentFrame()) - chain.push(ancestor); - chain.reverse(); - - if (chain.length === 1) - return this._describeMainFrame(page); - - const selectorPromises: Promise[] = []; - for (let i = 0; i < chain.length - 1; i++) - selectorPromises.push(findFrameSelector(chain[i + 1])); - - const result = await raceAgainstDeadline(() => Promise.all(selectorPromises), monotonicTime() + 2000); - if (!result.timedOut && result.result.every(selector => !!selector)) { - return { - pageAlias, - isMainFrame: false, - selectorsChain: result.result as string[], - }; - } - // Best effort to find a selector for the frame. - const selectorsChain = []; - for (let i = 0; i < chain.length - 1; i++) { - if (chain[i].name()) - selectorsChain.push(`iframe[name=${quoteCSSAttributeValue(chain[i].name())}]`); - else - selectorsChain.push(`iframe[src=${quoteCSSAttributeValue(chain[i].url())}]`); - } - return { - pageAlias, - isMainFrame: false, - selectorsChain, - }; - } - - testIdAttributeName(): string { - return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; - } - - private async _performAction(frame: Frame, action: actions.Action) { - // Commit last action so that no further signals are added to it. - this._generator.commitLastAction(); - - const frameDescription = await this._describeFrame(frame); - const actionInContext: ActionInContext = { - frame: frameDescription, - action - }; - - const perform = async (action: string, params: any, cb: (callMetadata: CallMetadata) => Promise) => { - const callMetadata: CallMetadata = { - id: `call@${createGuid()}`, - apiName: 'frame.' + action, - objectId: frame.guid, - pageId: frame._page.guid, - frameId: frame.guid, - startTime: monotonicTime(), - endTime: 0, - type: 'Frame', - method: action, - params, - log: [], - }; - this._generator.willPerformAction(actionInContext); - - try { - await frame.instrumentation.onBeforeCall(frame, callMetadata); - await cb(callMetadata); - } catch (e) { - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - this._generator.performedActionFailed(actionInContext); - return; - } - - callMetadata.endTime = monotonicTime(); - await frame.instrumentation.onAfterCall(frame, callMetadata); - - this._setCommittedAfterTimeout(actionInContext); - this._generator.didPerformAction(actionInContext); - }; - - const kActionTimeout = 5000; - if (action.name === 'click') { - const { options } = toClickOptions(action); - await perform('click', { selector: action.selector }, callMetadata => frame.click(callMetadata, action.selector, { ...options, timeout: kActionTimeout, strict: true })); - } - if (action.name === 'press') { - const modifiers = toModifiers(action.modifiers); - const shortcut = [...modifiers, action.key].join('+'); - await perform('press', { selector: action.selector, key: shortcut }, callMetadata => frame.press(callMetadata, action.selector, shortcut, { timeout: kActionTimeout, strict: true })); - } - if (action.name === 'check') - await perform('check', { selector: action.selector }, callMetadata => frame.check(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'uncheck') - await perform('uncheck', { selector: action.selector }, callMetadata => frame.uncheck(callMetadata, action.selector, { timeout: kActionTimeout, strict: true })); - if (action.name === 'select') { - const values = action.options.map(value => ({ value })); - await perform('selectOption', { selector: action.selector, values }, callMetadata => frame.selectOption(callMetadata, action.selector, [], values, { timeout: kActionTimeout, strict: true })); - } - } - - private async _recordAction(frame: Frame, action: actions.Action) { - // Commit last action so that no further signals are added to it. - this._generator.commitLastAction(); - - const frameDescription = await this._describeFrame(frame); - const actionInContext: ActionInContext = { - frame: frameDescription, - action - }; - this._setCommittedAfterTimeout(actionInContext); - this._generator.addAction(actionInContext); - } - - private _setCommittedAfterTimeout(actionInContext: ActionInContext) { - const timer = setTimeout(() => { - // Commit the action after 5 seconds so that no further signals are added to it. - actionInContext.committed = true; - this._timers.delete(timer); - }, isUnderTest() ? 500 : 5000); - this._timers.add(timer); - } - - private _onFrameNavigated(frame: Frame, page: Page) { - const pageAlias = this._pageAliases.get(page); - this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() }); - } - - private _onPopup(page: Page, popup: Page) { - const pageAlias = this._pageAliases.get(page)!; - const popupAlias = this._pageAliases.get(popup)!; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); - } - - private _onDownload(page: Page) { - const pageAlias = this._pageAliases.get(page)!; - ++this._lastDownloadOrdinal; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' }); - } - - private _onDialog(page: Page) { - const pageAlias = this._pageAliases.get(page)!; - ++this._lastDialogOrdinal; - this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' }); - } +function isScreenshotCommand(metadata: CallMetadata) { + return metadata.method.toLowerCase().includes('screenshot'); } function languageForFile(file: string) { @@ -703,49 +349,3 @@ function languageForFile(file: string) { return 'csharp'; return 'javascript'; } - -class ThrottledFile { - private _file: string; - private _timer: NodeJS.Timeout | undefined; - private _text: string | undefined; - - constructor(file: string) { - this._file = file; - } - - setContent(text: string) { - this._text = text; - if (!this._timer) - this._timer = setTimeout(() => this.flush(), 250); - } - - flush(): void { - if (this._timer) { - clearTimeout(this._timer); - this._timer = undefined; - } - if (this._text) - fs.writeFileSync(this._file, this._text); - this._text = undefined; - } -} - -function isScreenshotCommand(metadata: CallMetadata) { - return metadata.method.toLowerCase().includes('screenshot'); -} - -async function findFrameSelector(frame: Frame): Promise { - try { - const parent = frame.parentFrame(); - const frameElement = await frame.frameElement(); - if (!frameElement || !parent) - return; - const utility = await parent._utilityContext(); - const injected = await utility.injectedScript(); - const selector = await injected.evaluate((injected, element) => { - return injected.generateSelectorSimple(element as Element, { testIdAttributeName: '', omitInternalEngines: true }); - }, frameElement); - return selector; - } catch (e) { - } -} diff --git a/packages/playwright-core/src/server/recorder/DEPS.list b/packages/playwright-core/src/server/recorder/DEPS.list index 69c4226c68..22ec3dfc2f 100644 --- a/packages/playwright-core/src/server/recorder/DEPS.list +++ b/packages/playwright-core/src/server/recorder/DEPS.list @@ -1,8 +1,11 @@ [*] ../ +../codegen/language.ts +../codegen/languages.ts ../isomorphic/** ../registry/** ../../common/ +../../generated/recorderSource.ts ../../protocol/ ../../utils/** ../../utilsBundle.ts diff --git a/packages/playwright-core/src/server/recorder/contextRecorder.ts b/packages/playwright-core/src/server/recorder/contextRecorder.ts new file mode 100644 index 0000000000..17d2c2c130 --- /dev/null +++ b/packages/playwright-core/src/server/recorder/contextRecorder.ts @@ -0,0 +1,334 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type * as channels from '@protocol/channels'; +import type { Source } from '@recorder/recorderTypes'; +import { EventEmitter } from 'events'; +import * as recorderSource from '../../generated/recorderSource'; +import { eventsHelper, isUnderTest, monotonicTime, quoteCSSAttributeValue, type RegisteredListener } from '../../utils'; +import { raceAgainstDeadline } from '../../utils/timeoutRunner'; +import { BrowserContext } from '../browserContext'; +import type { ActionInContext, FrameDescription, LanguageGeneratorOptions, Language, LanguageGenerator } from '../codegen/types'; +import { languageSet } from '../codegen/languages'; +import type { Dialog } from '../dialog'; +import { Frame } from '../frames'; +import { Page } from '../page'; +import type * as actions from './recorderActions'; +import { performAction } from './recorderRunner'; +import { ThrottledFile } from './throttledFile'; +import { RecorderCollection } from './recorderCollection'; +import { generateCode } from '../codegen/language'; + +type BindingSource = { frame: Frame, page: Page }; + +export interface ContextRecorderDelegate { + rewriteActionInContext?(pageAliases: Map, actionInContext: ActionInContext): Promise; +} + +export class ContextRecorder extends EventEmitter { + static Events = { + Change: 'change' + }; + + private _collection: RecorderCollection; + private _pageAliases = new Map(); + private _lastPopupOrdinal = 0; + private _lastDialogOrdinal = -1; + private _lastDownloadOrdinal = -1; + private _timers = new Set(); + private _context: BrowserContext; + private _params: channels.BrowserContextRecorderSupplementEnableParams; + private _delegate: ContextRecorderDelegate; + private _recorderSources: Source[]; + private _throttledOutputFile: ThrottledFile | null = null; + private _orderedLanguages: LanguageGenerator[] = []; + private _listeners: RegisteredListener[] = []; + + constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams, delegate: ContextRecorderDelegate) { + super(); + this._context = context; + this._params = params; + this._delegate = delegate; + this._recorderSources = []; + const language = params.language || context.attribution.playwright.options.sdkLanguage; + this.setOutput(language, params.outputFile); + + // Make a copy of options to modify them later. + const languageGeneratorOptions: LanguageGeneratorOptions = { + browserName: context._browser.options.name, + launchOptions: { headless: false, ...params.launchOptions }, + contextOptions: { ...params.contextOptions }, + deviceName: params.device, + saveStorage: params.saveStorage, + }; + + const collection = new RecorderCollection(params.mode === 'recording'); + collection.on('change', () => { + this._recorderSources = []; + for (const languageGenerator of this._orderedLanguages) { + const { header, footer, actionTexts, text } = generateCode(collection.actions(), languageGenerator, languageGeneratorOptions); + const source: Source = { + isRecorded: true, + label: languageGenerator.name, + group: languageGenerator.groupName, + id: languageGenerator.id, + text, + header, + footer, + actions: actionTexts, + language: languageGenerator.highlighter, + highlight: [] + }; + source.revealLine = text.split('\n').length - 1; + this._recorderSources.push(source); + if (languageGenerator === this._orderedLanguages[0]) + this._throttledOutputFile?.setContent(source.text); + } + this.emit(ContextRecorder.Events.Change, { + sources: this._recorderSources, + primaryFileName: this._orderedLanguages[0].id + }); + }); + context.on(BrowserContext.Events.BeforeClose, () => { + this._throttledOutputFile?.flush(); + }); + this._listeners.push(eventsHelper.addEventListener(process, 'exit', () => { + this._throttledOutputFile?.flush(); + })); + this._collection = collection; + } + + setOutput(codegenId: string, outputFile?: string) { + const languages = languageSet(); + const primaryLanguage = [...languages].find(l => l.id === codegenId); + if (!primaryLanguage) + throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`); + languages.delete(primaryLanguage); + this._orderedLanguages = [primaryLanguage, ...languages]; + this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null; + this._collection?.restart(); + } + + languageName(id?: string): Language { + for (const lang of this._orderedLanguages) { + if (!id || lang.id === id) + return lang.highlighter; + } + return 'javascript'; + } + + async install() { + this._context.on(BrowserContext.Events.Page, (page: Page) => this._onPage(page)); + for (const page of this._context.pages()) + this._onPage(page); + this._context.on(BrowserContext.Events.Dialog, (dialog: Dialog) => this._onDialog(dialog.page())); + + // Input actions that potentially lead to navigation are intercepted on the page and are + // performed by the Playwright. + await this._context.exposeBinding('__pw_recorderPerformAction', false, + (source: BindingSource, action: actions.PerformOnRecordAction) => this._performAction(source.frame, action)); + + // Other non-essential actions are simply being recorded. + await this._context.exposeBinding('__pw_recorderRecordAction', false, + (source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)); + + await this._context.extendInjectedScript(recorderSource.source); + } + + setEnabled(enabled: boolean) { + this._collection.setEnabled(enabled); + } + + dispose() { + for (const timer of this._timers) + clearTimeout(timer); + this._timers.clear(); + eventsHelper.removeEventListeners(this._listeners); + } + + private async _onPage(page: Page) { + // First page is called page, others are called popup1, popup2, etc. + const frame = page.mainFrame(); + page.on('close', () => { + this._collection.addAction({ + frame: this._describeMainFrame(page), + committed: true, + action: { + name: 'closePage', + signals: [], + } + }); + this._pageAliases.delete(page); + }); + frame.on(Frame.Events.InternalNavigation, event => { + if (event.isPublic) + this._onFrameNavigated(frame, page); + }); + page.on(Page.Events.Download, () => this._onDownload(page)); + const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : ''; + const pageAlias = 'page' + suffix; + this._pageAliases.set(page, pageAlias); + + if (page.opener()) { + this._onPopup(page.opener()!, page); + } else { + this._collection.addAction({ + frame: this._describeMainFrame(page), + committed: true, + action: { + name: 'openPage', + url: page.mainFrame().url(), + signals: [], + } + }); + } + } + + clearScript(): void { + this._collection.restart(); + if (this._params.mode === 'recording') { + for (const page of this._context.pages()) + this._onFrameNavigated(page.mainFrame(), page); + } + } + + private _describeMainFrame(page: Page): FrameDescription { + return { + pageAlias: this._pageAliases.get(page)!, + framePath: [], + }; + } + + private async _describeFrame(frame: Frame): Promise { + return { + pageAlias: this._pageAliases.get(frame._page)!, + framePath: await generateFrameSelector(frame), + }; + } + + testIdAttributeName(): string { + return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid'; + } + + private async _performAction(frame: Frame, action: actions.PerformOnRecordAction) { + // Commit last action so that no further signals are added to it. + this._collection.commitLastAction(); + + const frameDescription = await this._describeFrame(frame); + const actionInContext: ActionInContext = { + frame: frameDescription, + action, + description: undefined, + }; + + await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext); + + this._collection.willPerformAction(actionInContext); + const success = await performAction(this._pageAliases, actionInContext); + if (success) { + this._collection.didPerformAction(actionInContext); + this._setCommittedAfterTimeout(actionInContext); + } else { + this._collection.performedActionFailed(actionInContext); + } + } + + private async _recordAction(frame: Frame, action: actions.Action) { + // Commit last action so that no further signals are added to it. + this._collection.commitLastAction(); + + const frameDescription = await this._describeFrame(frame); + const actionInContext: ActionInContext = { + frame: frameDescription, + action, + description: undefined, + }; + + await this._delegate.rewriteActionInContext?.(this._pageAliases, actionInContext); + + this._setCommittedAfterTimeout(actionInContext); + this._collection.addAction(actionInContext); + } + + private _setCommittedAfterTimeout(actionInContext: ActionInContext) { + const timer = setTimeout(() => { + // Commit the action after 5 seconds so that no further signals are added to it. + actionInContext.committed = true; + this._timers.delete(timer); + }, isUnderTest() ? 500 : 5000); + this._timers.add(timer); + } + + private _onFrameNavigated(frame: Frame, page: Page) { + const pageAlias = this._pageAliases.get(page); + this._collection.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() }); + } + + private _onPopup(page: Page, popup: Page) { + const pageAlias = this._pageAliases.get(page)!; + const popupAlias = this._pageAliases.get(popup)!; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); + } + + private _onDownload(page: Page) { + const pageAlias = this._pageAliases.get(page)!; + ++this._lastDownloadOrdinal; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'download', downloadAlias: this._lastDownloadOrdinal ? String(this._lastDownloadOrdinal) : '' }); + } + + private _onDialog(page: Page) { + const pageAlias = this._pageAliases.get(page)!; + ++this._lastDialogOrdinal; + this._collection.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: this._lastDialogOrdinal ? String(this._lastDialogOrdinal) : '' }); + } +} + +export async function generateFrameSelector(frame: Frame): Promise { + const selectorPromises: Promise[] = []; + while (frame) { + const parent = frame.parentFrame(); + if (!parent) + break; + selectorPromises.push(generateFrameSelectorInParent(parent, frame)); + frame = parent; + } + const result = await Promise.all(selectorPromises); + return result.reverse(); +} + +async function generateFrameSelectorInParent(parent: Frame, frame: Frame): Promise { + const result = await raceAgainstDeadline(async () => { + try { + const frameElement = await frame.frameElement(); + if (!frameElement || !parent) + return; + const utility = await parent._utilityContext(); + const injected = await utility.injectedScript(); + const selector = await injected.evaluate((injected, element) => { + return injected.generateSelectorSimple(element as Element); + }, frameElement); + return selector; + } catch (e) { + return e.toString(); + } + }, monotonicTime() + 2000); + if (!result.timedOut && result.result) + return result.result; + + if (frame.name()) + return `iframe[name=${quoteCSSAttributeValue(frame.name())}]`; + return `iframe[src=${quoteCSSAttributeValue(frame.url())}]`; +} diff --git a/packages/playwright-core/src/server/recorder/language.ts b/packages/playwright-core/src/server/recorder/language.ts deleted file mode 100644 index cee2b22163..0000000000 --- a/packages/playwright-core/src/server/recorder/language.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { BrowserContextOptions, LaunchOptions } from '../../..'; -import type { Language } from '../../utils'; -import type { ActionInContext } from './codeGenerator'; -import type { Action, DialogSignal, DownloadSignal, PopupSignal } from './recorderActions'; -export type { Language } from '../../utils'; - -export type LanguageGeneratorOptions = { - browserName: string; - launchOptions: LaunchOptions; - contextOptions: BrowserContextOptions; - deviceName?: string; - saveStorage?: string; -}; - -export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text'; -export type LocatorBase = 'page' | 'locator' | 'frame-locator'; - -export interface LanguageGenerator { - id: string; - groupName: string; - name: string; - highlighter: Language; - generateHeader(options: LanguageGeneratorOptions): string; - generateAction(actionInContext: ActionInContext): string; - generateFooter(saveStorage: string | undefined): string; -} - -export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions { - // Filter out all the properties from the device descriptor. - const cleanedOptions: Record = {}; - for (const property in options) { - if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property])) - cleanedOptions[property] = (options as any)[property]; - } - return cleanedOptions; -} - -export function toSignalMap(action: Action) { - let popup: PopupSignal | undefined; - let download: DownloadSignal | undefined; - let dialog: DialogSignal | undefined; - for (const signal of action.signals) { - if (signal.name === 'popup') - popup = signal; - else if (signal.name === 'download') - download = signal; - else if (signal.name === 'dialog') - dialog = signal; - } - return { - popup, - download, - dialog, - }; -} diff --git a/packages/playwright-core/src/server/recorder/recorderActions.ts b/packages/playwright-core/src/server/recorder/recorderActions.ts index 3c9720cbc4..9447f32457 100644 --- a/packages/playwright-core/src/server/recorder/recorderActions.ts +++ b/packages/playwright-core/src/server/recorder/recorderActions.ts @@ -37,28 +37,28 @@ export type ActionBase = { signals: Signal[], }; -export type ClickAction = ActionBase & { - name: 'click', +export type ActionWithSelector = ActionBase & { selector: string, +}; + +export type ClickAction = ActionWithSelector & { + name: 'click', button: 'left' | 'middle' | 'right', modifiers: number, clickCount: number, position?: Point, }; -export type CheckAction = ActionBase & { +export type CheckAction = ActionWithSelector & { name: 'check', - selector: string, }; -export type UncheckAction = ActionBase & { +export type UncheckAction = ActionWithSelector & { name: 'uncheck', - selector: string, }; -export type FillAction = ActionBase & { +export type FillAction = ActionWithSelector & { name: 'fill', - selector: string, text: string, }; @@ -83,44 +83,39 @@ export type PressAction = ActionBase & { modifiers: number, }; -export type SelectAction = ActionBase & { +export type SelectAction = ActionWithSelector & { name: 'select', - selector: string, options: string[], }; -export type SetInputFilesAction = ActionBase & { +export type SetInputFilesAction = ActionWithSelector & { name: 'setInputFiles', - selector: string, files: string[], }; -export type AssertTextAction = ActionBase & { +export type AssertTextAction = ActionWithSelector & { name: 'assertText', - selector: string, text: string, substring: boolean, }; -export type AssertValueAction = ActionBase & { +export type AssertValueAction = ActionWithSelector & { name: 'assertValue', - selector: string, value: string, }; -export type AssertCheckedAction = ActionBase & { +export type AssertCheckedAction = ActionWithSelector & { name: 'assertChecked', - selector: string, checked: boolean, }; -export type AssertVisibleAction = ActionBase & { +export type AssertVisibleAction = ActionWithSelector & { name: 'assertVisible', - selector: string, }; export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction; export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction; +export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction; // Signals. @@ -148,14 +143,3 @@ export type DialogSignal = BaseSignal & { }; export type Signal = NavigationSignal | PopupSignal | DownloadSignal | DialogSignal; - -type FrameDescriptionMainFrame = { - isMainFrame: true; -}; - -type FrameDescriptionChildFrame = { - isMainFrame: false; - selectorsChain: string[]; -}; - -export type FrameDescription = { pageAlias: string } & (FrameDescriptionMainFrame | FrameDescriptionChildFrame); diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index 7f9166ae73..0faf191ea5 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -24,7 +24,7 @@ import type { CallLog, EventData, Mode, Source } from '@recorder/recorderTypes'; import { isUnderTest } from '../../utils'; import { mime } from '../../utilsBundle'; import { syncLocalStorageWithSettings } from '../launchApp'; -import type { Recorder } from '../recorder'; +import type { Recorder, RecorderAppFactory } from '../recorder'; import type { BrowserContext } from '../browserContext'; import { launchApp } from '../launchApp'; @@ -113,7 +113,15 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { await mainFrame.goto(serverSideCallMetadata(), 'https://playwright/index.html'); } - static async open(recorder: Recorder, inspectedContext: BrowserContext, handleSIGINT: boolean | undefined): Promise { + static factory(context: BrowserContext): RecorderAppFactory { + return async recorder => { + if (process.env.PW_CODEGEN_NO_INSPECTOR) + return new EmptyRecorderApp(); + return await RecorderApp._open(recorder, context); + }; + } + + private static async _open(recorder: Recorder, inspectedContext: BrowserContext): Promise { const sdkLanguage = inspectedContext.attribution.playwright.options.sdkLanguage; const headed = !!inspectedContext._browser.options.headful; const recorderPlaywright = (require('../playwright').createPlaywright as typeof import('../playwright').createPlaywright)({ sdkLanguage: 'javascript', isInternalPlaywright: true }); @@ -125,7 +133,7 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { noDefaultViewport: true, headless: !!process.env.PWTEST_CLI_HEADLESS || (isUnderTest() && !headed), useWebSocket: !!process.env.PWTEST_RECORDER_PORT, - handleSIGINT, + handleSIGINT: false, args: process.env.PWTEST_RECORDER_PORT ? [`--remote-debugging-port=${process.env.PWTEST_RECORDER_PORT}`] : [], executablePath: inspectedContext._browser.options.isChromium ? inspectedContext._browser.options.customExecutablePath : undefined, } @@ -170,11 +178,11 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { async setSelector(selector: string, userGesture?: boolean): Promise { if (userGesture) { - if (this._recorder.mode() === 'inspecting') { + if (this._recorder?.mode() === 'inspecting') { this._recorder.setMode('standby'); this._page.bringToFront(); } else { - this._recorder.setMode('recording'); + this._recorder?.setMode('recording'); } } await this._page.mainFrame().evaluateExpression(((data: { selector: string, userGesture?: boolean }) => { diff --git a/packages/playwright-core/src/server/recorder/codeGenerator.ts b/packages/playwright-core/src/server/recorder/recorderCollection.ts similarity index 76% rename from packages/playwright-core/src/server/recorder/codeGenerator.ts rename to packages/playwright-core/src/server/recorder/recorderCollection.ts index d3bb5f86d9..29da778ffb 100644 --- a/packages/playwright-core/src/server/recorder/codeGenerator.ts +++ b/packages/playwright-core/src/server/recorder/recorderCollection.ts @@ -15,32 +15,19 @@ */ import { EventEmitter } from 'events'; -import type { BrowserContextOptions, LaunchOptions } from '../../..'; import type { Frame } from '../frames'; -import type { LanguageGenerator, LanguageGeneratorOptions } from './language'; -import type { Action, Signal, FrameDescription } from './recorderActions'; +import type { Signal } from './recorderActions'; +import type { ActionInContext } from '../codegen/types'; -export type ActionInContext = { - frame: FrameDescription; - action: Action; - committed?: boolean; -}; - -export class CodeGenerator extends EventEmitter { +export class RecorderCollection extends EventEmitter { private _currentAction: ActionInContext | null = null; private _lastAction: ActionInContext | null = null; private _actions: ActionInContext[] = []; private _enabled: boolean; - private _options: LanguageGeneratorOptions; - constructor(browserName: string, enabled: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName: string | undefined, saveStorage: string | undefined) { + constructor(enabled: boolean) { super(); - - // Make a copy of options to modify them later. - launchOptions = { headless: false, ...launchOptions }; - contextOptions = { ...contextOptions }; this._enabled = enabled; - this._options = { browserName, launchOptions, contextOptions, deviceName, saveStorage }; this.restart(); } @@ -51,6 +38,10 @@ export class CodeGenerator extends EventEmitter { this.emit('change'); } + actions() { + return this._actions; + } + setEnabled(enabled: boolean) { this._enabled = enabled; } @@ -146,7 +137,7 @@ export class CodeGenerator extends EventEmitter { this.addAction({ frame: { pageAlias, - isMainFrame: true, + framePath: [], }, committed: true, action: { @@ -157,12 +148,4 @@ export class CodeGenerator extends EventEmitter { }); } } - - generateStructure(languageGenerator: LanguageGenerator) { - const header = languageGenerator.generateHeader(this._options); - const footer = languageGenerator.generateFooter(this._options.saveStorage); - const actions = this._actions.map(a => languageGenerator.generateAction(a)).filter(Boolean); - const text = [header, ...actions, footer].join('\n'); - return { header, footer, actions, text }; - } } diff --git a/packages/playwright-core/src/server/recorder/recorderRunner.ts b/packages/playwright-core/src/server/recorder/recorderRunner.ts new file mode 100644 index 0000000000..b6bdfd1a72 --- /dev/null +++ b/packages/playwright-core/src/server/recorder/recorderRunner.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createGuid, monotonicTime, serializeExpectedTextValues } from '../../utils'; +import { toClickOptions, toKeyboardModifiers } from '../codegen/language'; +import type { ActionInContext } from '../codegen/types'; +import type { Frame } from '../frames'; +import type { CallMetadata } from '../instrumentation'; +import type { Page } from '../page'; +import { buildFullSelector } from './recorderUtils'; + +async function innerPerformAction(mainFrame: Frame, action: string, params: any, cb: (callMetadata: CallMetadata) => Promise): Promise { + const callMetadata: CallMetadata = { + id: `call@${createGuid()}`, + apiName: 'frame.' + action, + objectId: mainFrame.guid, + pageId: mainFrame._page.guid, + frameId: mainFrame.guid, + startTime: monotonicTime(), + endTime: 0, + type: 'Frame', + method: action, + params, + log: [], + }; + + try { + await mainFrame.instrumentation.onBeforeCall(mainFrame, callMetadata); + await cb(callMetadata); + } catch (e) { + callMetadata.endTime = monotonicTime(); + await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata); + return false; + } + + callMetadata.endTime = monotonicTime(); + await mainFrame.instrumentation.onAfterCall(mainFrame, callMetadata); + return true; +} + +export async function performAction(pageAliases: Map, actionInContext: ActionInContext): Promise { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + const mainFrame = page.mainFrame(); + const { action } = actionInContext; + const kActionTimeout = 5000; + + if (action.name === 'navigate') + return await innerPerformAction(mainFrame, 'goto', { url: action.url }, callMetadata => mainFrame.goto(callMetadata, action.url, { timeout: kActionTimeout })); + if (action.name === 'openPage') + throw Error('Not reached'); + if (action.name === 'closePage') + return await innerPerformAction(mainFrame, 'close', {}, callMetadata => mainFrame._page.close(callMetadata)); + + const selector = buildFullSelector(actionInContext.frame.framePath, action.selector); + + if (action.name === 'click') { + const options = toClickOptions(action); + return await innerPerformAction(mainFrame, 'click', { selector }, callMetadata => mainFrame.click(callMetadata, selector, { ...options, timeout: kActionTimeout, strict: true })); + } + if (action.name === 'press') { + const modifiers = toKeyboardModifiers(action.modifiers); + const shortcut = [...modifiers, action.key].join('+'); + return await innerPerformAction(mainFrame, 'press', { selector, key: shortcut }, callMetadata => mainFrame.press(callMetadata, selector, shortcut, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'fill') + return await innerPerformAction(mainFrame, 'fill', { selector, text: action.text }, callMetadata => mainFrame.fill(callMetadata, selector, action.text, { timeout: kActionTimeout, strict: true })); + if (action.name === 'setInputFiles') + return await innerPerformAction(mainFrame, 'setInputFiles', { selector, files: action.files }, callMetadata => mainFrame.setInputFiles(callMetadata, selector, { selector, payloads: [], timeout: kActionTimeout, strict: true })); + if (action.name === 'check') + return await innerPerformAction(mainFrame, 'check', { selector }, callMetadata => mainFrame.check(callMetadata, selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'uncheck') + return await innerPerformAction(mainFrame, 'uncheck', { selector }, callMetadata => mainFrame.uncheck(callMetadata, selector, { timeout: kActionTimeout, strict: true })); + if (action.name === 'select') { + const values = action.options.map(value => ({ value })); + return await innerPerformAction(mainFrame, 'selectOption', { selector, values }, callMetadata => mainFrame.selectOption(callMetadata, selector, [], values, { timeout: kActionTimeout, strict: true })); + } + if (action.name === 'assertChecked') { + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, + expression: 'to.be.checked', + isNot: !action.checked, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertText') { + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, + expression: 'to.have.text', + expectedText: serializeExpectedTextValues([action.text], { matchSubstring: true, normalizeWhiteSpace: true }), + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertValue') { + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, + expression: 'to.have.value', + expectedValue: action.value, + isNot: false, + timeout: kActionTimeout, + })); + } + if (action.name === 'assertVisible') { + return await innerPerformAction(mainFrame, 'expect', { selector }, callMetadata => mainFrame.expect(callMetadata, selector, { + selector, + expression: 'to.be.visible', + isNot: false, + timeout: kActionTimeout, + })); + } + throw new Error('Internal error: unexpected action ' + (action as any).name); +} diff --git a/packages/playwright-core/src/server/recorder/recorderUtils.ts b/packages/playwright-core/src/server/recorder/recorderUtils.ts index d6237b4899..b4949115d2 100644 --- a/packages/playwright-core/src/server/recorder/recorderUtils.ts +++ b/packages/playwright-core/src/server/recorder/recorderUtils.ts @@ -16,6 +16,10 @@ import type { CallMetadata } from '../instrumentation'; import type { CallLog, CallLogStatus } from '@recorder/recorderTypes'; +import type { Page } from '../page'; +import type { ActionInContext } from '../codegen/types'; +import type { Frame } from '../frames'; +import type * as actions from './recorderActions'; export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus): CallLog { let title = metadata.apiName || metadata.method; @@ -44,3 +48,27 @@ export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus) }; return callLog; } + +export function buildFullSelector(framePath: string[], selector: string) { + return [...framePath, selector].join(' >> internal:control=enter-frame >> '); +} + +export function mainFrameForAction(pageAliases: Map, actionInContext: ActionInContext): Frame { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + return page.mainFrame(); +} + +export async function frameForAction(pageAliases: Map, actionInContext: ActionInContext, action: actions.ActionWithSelector): Promise { + const pageAlias = actionInContext.frame.pageAlias; + const page = [...pageAliases.entries()].find(([, alias]) => pageAlias === alias)?.[0]; + if (!page) + throw new Error('Internal error: page not found'); + const fullSelector = buildFullSelector(actionInContext.frame.framePath, action.selector); + const result = await page.mainFrame().selectors.resolveFrameForSelector(fullSelector); + if (!result) + throw new Error('Internal error: frame not found'); + return result.frame; +} diff --git a/packages/playwright-core/src/server/recorder/throttledFile.ts b/packages/playwright-core/src/server/recorder/throttledFile.ts new file mode 100644 index 0000000000..4a34f41a0c --- /dev/null +++ b/packages/playwright-core/src/server/recorder/throttledFile.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; + +export class ThrottledFile { + private _file: string; + private _timer: NodeJS.Timeout | undefined; + private _text: string | undefined; + + constructor(file: string) { + this._file = file; + } + + setContent(text: string) { + this._text = text; + if (!this._timer) + this._timer = setTimeout(() => this.flush(), 250); + } + + flush(): void { + if (this._timer) { + clearTimeout(this._timer); + this._timer = undefined; + } + if (this._text) + fs.writeFileSync(this._file, this._text); + this._text = undefined; + } +} diff --git a/packages/playwright-core/src/server/recorder/utils.ts b/packages/playwright-core/src/server/recorder/utils.ts deleted file mode 100644 index 883a8ab129..0000000000 --- a/packages/playwright-core/src/server/recorder/utils.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { Frame } from '../frames'; -import type { SmartKeyboardModifier } from '../types'; -import type * as actions from './recorderActions'; - -export type MouseClickOptions = Parameters[2]; - -export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } { - let method: 'click' | 'dblclick' = 'click'; - if (action.clickCount === 2) - method = 'dblclick'; - const modifiers = toModifiers(action.modifiers); - const options: MouseClickOptions = {}; - if (action.button !== 'left') - options.button = action.button; - if (modifiers.length) - options.modifiers = modifiers; - if (action.clickCount > 2) - options.clickCount = action.clickCount; - if (action.position) - options.position = action.position; - return { method, options }; -} - -export function toModifiers(modifiers: number): SmartKeyboardModifier[] { - const result: SmartKeyboardModifier[] = []; - if (modifiers & 1) - result.push('Alt'); - if (modifiers & 2) - result.push('ControlOrMeta'); - if (modifiers & 4) - result.push('ControlOrMeta'); - if (modifiers & 8) - result.push('Shift'); - return result; -} diff --git a/packages/playwright-core/src/server/webkit/protocol.d.ts b/packages/playwright-core/src/server/webkit/protocol.d.ts index 80b30f28fb..3ddfda4627 100644 --- a/packages/playwright-core/src/server/webkit/protocol.d.ts +++ b/packages/playwright-core/src/server/webkit/protocol.d.ts @@ -536,7 +536,7 @@ export module Protocol { /** * Pseudo-style identifier (see enum PseudoId in RenderStyleConstants.h). */ - export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner"; + export type PseudoId = "first-line"|"first-letter"|"grammar-error"|"highlight"|"marker"|"before"|"after"|"selection"|"backdrop"|"spelling-error"|"target-text"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"-webkit-scrollbar"|"-webkit-resizer"|"-webkit-scrollbar-thumb"|"-webkit-scrollbar-button"|"-webkit-scrollbar-track"|"-webkit-scrollbar-track-piece"|"-webkit-scrollbar-corner"; /** * Pseudo-style identifier (see enum PseudoId in RenderStyleConstants.h). */ diff --git a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts index a450127789..93367726ed 100644 --- a/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts +++ b/packages/playwright-core/src/server/webkit/wkInterceptableRequest.ts @@ -141,7 +141,7 @@ export class WKRouteImpl implements network.RouteDelegate { }); } - async continue(request: network.Request, overrides: types.NormalizedContinueOverrides) { + async continue(overrides: types.NormalizedContinueOverrides) { // In certain cases, protocol will return error if the request was already canceled // or the page was closed. We should tolerate these errors. await this._session.sendMayFail('Network.interceptWithRequest', { diff --git a/packages/playwright-core/src/utils/expectUtils.ts b/packages/playwright-core/src/utils/expectUtils.ts new file mode 100644 index 0000000000..0ae21e8602 --- /dev/null +++ b/packages/playwright-core/src/utils/expectUtils.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { ExpectedTextValue } from '@protocol/channels'; +import { isRegExp, isString } from './rtti'; + +export function serializeExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { + return items.map(i => ({ + string: isString(i) ? i : undefined, + regexSource: isRegExp(i) ? i.source : undefined, + regexFlags: isRegExp(i) ? i.flags : undefined, + matchSubstring: options.matchSubstring, + ignoreCase: options.ignoreCase, + normalizeWhiteSpace: options.normalizeWhiteSpace, + })); +} diff --git a/packages/playwright-core/src/utils/index.ts b/packages/playwright-core/src/utils/index.ts index 372922ec59..0bc7a75b08 100644 --- a/packages/playwright-core/src/utils/index.ts +++ b/packages/playwright-core/src/utils/index.ts @@ -21,6 +21,7 @@ export * from './debug'; export * from './debugLogger'; export * from './env'; export * from './eventsHelper'; +export * from './expectUtils'; export * from './fileUtils'; export * from './headers'; export * from './hostPlatform'; diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index bb037a1ca7..a2a62be867 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -19,6 +19,7 @@ import path from 'path'; export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors; export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug; +export const dotenv: typeof import('../bundles/utils/node_modules/dotenv') = require('./utilsBundleImpl').dotenv; export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl; export const HttpsProxyAgent: typeof import('../bundles/utils/node_modules/https-proxy-agent').HttpsProxyAgent = require('./utilsBundleImpl').HttpsProxyAgent; export const jpegjs: typeof import('../bundles/utils/node_modules/jpeg-js') = require('./utilsBundleImpl').jpegjs; diff --git a/packages/playwright-core/types/protocol.d.ts b/packages/playwright-core/types/protocol.d.ts index 99ce1d3a6a..caadb2a577 100644 --- a/packages/playwright-core/types/protocol.d.ts +++ b/packages/playwright-core/types/protocol.d.ts @@ -1131,17 +1131,21 @@ using Audits.issueAdded event. } /** - * Defines commands and events for browser extensions. Available if the client -is connected using the --remote-debugging-pipe flag and -the --enable-unsafe-extension-debugging flag is set. + * Defines commands and events for browser extensions. */ export module Extensions { + /** + * Storage areas. + */ + export type StorageArea = "session"|"local"|"sync"|"managed"; /** * Installs an unpacked extension from the filesystem similar to --load-extension CLI flags. Returns extension ID once the extension -has been installed. +has been installed. Available if the client is connected using the +--remote-debugging-pipe flag and the --enable-unsafe-extension-debugging +flag is set. */ export type loadUnpackedParameters = { /** @@ -1155,6 +1159,81 @@ has been installed. */ id: string; } + /** + * Gets data from extension storage in the given `storageArea`. If `keys` is +specified, these are used to filter the result. + */ + export type getStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to retrieve data from. + */ + storageArea: StorageArea; + /** + * Keys to retrieve. + */ + keys?: string[]; + } + export type getStorageItemsReturnValue = { + data: { [key: string]: string }; + } + /** + * Removes `keys` from extension storage in the given `storageArea`. + */ + export type removeStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + /** + * Keys to remove. + */ + keys: string[]; + } + export type removeStorageItemsReturnValue = { + } + /** + * Clears extension storage in the given `storageArea`. + */ + export type clearStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to remove data from. + */ + storageArea: StorageArea; + } + export type clearStorageItemsReturnValue = { + } + /** + * Sets `values` in extension storage in the given `storageArea`. The provided `values` +will be merged with existing values in the storage area. + */ + export type setStorageItemsParameters = { + /** + * ID of extension. + */ + id: string; + /** + * StorageArea to set data in. + */ + storageArea: StorageArea; + /** + * Values to set. + */ + values: { [key: string]: string }; + } + export type setStorageItemsReturnValue = { + } } /** @@ -2532,16 +2611,6 @@ stylesheet rules) this rule came from. */ style: CSSStyle; } - /** - * CSS position-fallback rule representation. - */ - export interface CSSPositionFallbackRule { - name: Value; - /** - * List of keyframes. - */ - tryRules: CSSTryRule[]; - } /** * CSS @position-try rule representation. */ @@ -2888,10 +2957,6 @@ attributes) for a DOM node identified by `nodeId`. * A list of CSS keyframed animations matching this node. */ cssKeyframesRules?: CSSKeyframesRule[]; - /** - * A list of CSS position fallbacks matching this node. - */ - cssPositionFallbackRules?: CSSPositionFallbackRule[]; /** * A list of CSS @position-try rules matching this node, based on the position-try-fallbacks property. */ @@ -3496,7 +3561,7 @@ front-end. /** * Pseudo element type. */ - export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; + export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"; /** * Shadow root type. */ @@ -3646,6 +3711,13 @@ The property is always undefined now. compatibilityMode?: CompatibilityMode; assignedSlot?: BackendNode; } + /** + * A structure to hold the top-level node of a detached tree and an array of its retained descendants. + */ + export interface DetachedElementInfo { + treeNode: Node; + retainedNodeIds: NodeId[]; + } /** * A structure holding an RGBA color. */ @@ -4693,6 +4765,17 @@ File wrapper. export type getFileInfoReturnValue = { path: string; } + /** + * Returns list of detached nodes + */ + export type getDetachedDomNodesParameters = { + } + export type getDetachedDomNodesReturnValue = { + /** + * The list of detached nodes + */ + detachedNodes: DetachedElementInfo[]; + } /** * Enables console to refer to the node with given id via $x (see Command Line API for more details $x functions). @@ -11369,7 +11452,7 @@ as an ad. * All Permissions Policy features. This enum should match the one defined in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5. */ - export type PermissionsPolicyFeature = "accelerometer"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; + export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking"; /** * Reason for a permissions policy feature to be disabled. */ @@ -11784,7 +11867,7 @@ Example URLs: http://www.google.com/file.html -> "google.com" */ fixed?: number; } - export type ClientNavigationReason = "formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"scriptInitiated"|"metaTagRefresh"|"pageBlockInterstitial"|"reload"|"anchorClick"; + export type ClientNavigationReason = "anchorClick"|"formSubmissionGet"|"formSubmissionPost"|"httpHeaderRefresh"|"initialFrameNavigation"|"metaTagRefresh"|"other"|"pageBlockInterstitial"|"reload"|"scriptInitiated"; export type ClientNavigationDisposition = "currentTab"|"newTab"|"newWindow"|"download"; export interface InstallabilityErrorArgument { /** @@ -12298,6 +12381,10 @@ when bfcache navigation fails. * Frame's new url. */ url: string; + /** + * Navigation type + */ + navigationType: "fragment"|"historyApi"|"other"; } /** * Compressed image data requested by the `startScreencast`. @@ -16922,7 +17009,7 @@ possible for multiple rule sets and links to trigger a single attempt. /** * List of FinalStatus reasons for Prerender2. */ - export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"; + export type PrerenderFinalStatus = "Activated"|"Destroyed"|"LowEndDevice"|"InvalidSchemeRedirect"|"InvalidSchemeNavigation"|"NavigationRequestBlockedByCsp"|"MainFrameNavigation"|"MojoBinderPolicy"|"RendererProcessCrashed"|"RendererProcessKilled"|"Download"|"TriggerDestroyed"|"NavigationNotCommitted"|"NavigationBadHttpStatus"|"ClientCertRequested"|"NavigationRequestNetworkError"|"CancelAllHostsForTesting"|"DidFailLoad"|"Stop"|"SslCertificateError"|"LoginAuthRequested"|"UaChangeRequiresReload"|"BlockedByClient"|"AudioOutputDeviceRequested"|"MixedContent"|"TriggerBackgrounded"|"MemoryLimitExceeded"|"DataSaverEnabled"|"TriggerUrlHasEffectiveUrl"|"ActivatedBeforeStarted"|"InactivePageRestriction"|"StartFailed"|"TimeoutBackgrounded"|"CrossSiteRedirectInInitialNavigation"|"CrossSiteNavigationInInitialNavigation"|"SameSiteCrossOriginRedirectNotOptInInInitialNavigation"|"SameSiteCrossOriginNavigationNotOptInInInitialNavigation"|"ActivationNavigationParameterMismatch"|"ActivatedInBackground"|"EmbedderHostDisallowed"|"ActivationNavigationDestroyedBeforeSuccess"|"TabClosedByUserGesture"|"TabClosedWithoutUserGesture"|"PrimaryMainFrameRendererProcessCrashed"|"PrimaryMainFrameRendererProcessKilled"|"ActivationFramePolicyNotCompatible"|"PreloadingDisabled"|"BatterySaverEnabled"|"ActivatedDuringMainFrameNavigation"|"PreloadingUnsupportedByWebContents"|"CrossSiteRedirectInMainFrameNavigation"|"CrossSiteNavigationInMainFrameNavigation"|"SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation"|"SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation"|"MemoryPressureOnTrigger"|"MemoryPressureAfterTriggered"|"PrerenderingDisabledByDevTools"|"SpeculationRuleRemoved"|"ActivatedWithAuxiliaryBrowsingContexts"|"MaxNumOfRunningEagerPrerendersExceeded"|"MaxNumOfRunningNonEagerPrerendersExceeded"|"MaxNumOfRunningEmbedderPrerendersExceeded"|"PrerenderingUrlHasEffectiveUrl"|"RedirectedPrerenderingUrlHasEffectiveUrl"|"ActivationUrlHasEffectiveUrl"|"JavaScriptInterfaceAdded"|"JavaScriptInterfaceRemoved"|"AllPrerenderingCanceled"|"WindowClosed"|"SlowNetwork"|"OtherPrerenderedPageActivated"; /** * Preloading status values, see also PreloadingTriggeringOutcome. This status is shared by prefetchStatusUpdated and prerenderStatusUpdated. @@ -17270,6 +17357,101 @@ supported yet. } } + /** + * This domain allows configuring virtual Bluetooth devices to test +the web-bluetooth API. + */ + export module BluetoothEmulation { + /** + * Indicates the various states of Central. + */ + export type CentralState = "absent"|"powered-off"|"powered-on"; + /** + * Stores the manufacturer data + */ + export interface ManufacturerData { + /** + * Company identifier +https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +https://usb.org/developers + */ + key: number; + /** + * Manufacturer-specific data + */ + data: binary; + } + /** + * Stores the byte data of the advertisement packet sent by a Bluetooth device. + */ + export interface ScanRecord { + name?: string; + uuids?: string[]; + /** + * Stores the external appearance description of the device. + */ + appearance?: number; + /** + * Stores the transmission power of a broadcasting device. + */ + txPower?: number; + /** + * Key is the company identifier and the value is an array of bytes of +manufacturer specific data. + */ + manufacturerData?: ManufacturerData[]; + } + /** + * Stores the advertisement packet information that is sent by a Bluetooth device. + */ + export interface ScanEntry { + deviceAddress: string; + rssi: number; + scanRecord: ScanRecord; + } + + + /** + * Enable the BluetoothEmulation domain. + */ + export type enableParameters = { + /** + * State of the simulated central. + */ + state: CentralState; + } + export type enableReturnValue = { + } + /** + * Disable the BluetoothEmulation domain. + */ + export type disableParameters = { + } + export type disableReturnValue = { + } + /** + * Simulates a peripheral with |address|, |name| and |knownServiceUuids| +that has already been connected to the system. + */ + export type simulatePreconnectedPeripheralParameters = { + address: string; + name: string; + manufacturerData: ManufacturerData[]; + knownServiceUuids: string[]; + } + export type simulatePreconnectedPeripheralReturnValue = { + } + /** + * Simulates an advertisement packet described in |entry| being received by +the central. + */ + export type simulateAdvertisementParameters = { + entry: ScanEntry; + } + export type simulateAdvertisementReturnValue = { + } + } + /** * This domain is deprecated - use Runtime or Log instead. */ @@ -20122,6 +20304,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastParameters; "Audits.checkFormsIssues": Audits.checkFormsIssuesParameters; "Extensions.loadUnpacked": Extensions.loadUnpackedParameters; + "Extensions.getStorageItems": Extensions.getStorageItemsParameters; + "Extensions.removeStorageItems": Extensions.removeStorageItemsParameters; + "Extensions.clearStorageItems": Extensions.clearStorageItemsParameters; + "Extensions.setStorageItems": Extensions.setStorageItemsParameters; "Autofill.trigger": Autofill.triggerParameters; "Autofill.setAddresses": Autofill.setAddressesParameters; "Autofill.disable": Autofill.disableParameters; @@ -20232,6 +20418,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledParameters; "DOM.getNodeStackTraces": DOM.getNodeStackTracesParameters; "DOM.getFileInfo": DOM.getFileInfoParameters; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesParameters; "DOM.setInspectedNode": DOM.setInspectedNodeParameters; "DOM.setNodeName": DOM.setNodeNameParameters; "DOM.setNodeValue": DOM.setNodeValueParameters; @@ -20616,6 +20803,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppParameters; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppParameters; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsParameters; + "BluetoothEmulation.enable": BluetoothEmulation.enableParameters; + "BluetoothEmulation.disable": BluetoothEmulation.disableParameters; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralParameters; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementParameters; "Console.clearMessages": Console.clearMessagesParameters; "Console.disable": Console.disableParameters; "Console.enable": Console.enableParameters; @@ -20722,6 +20913,10 @@ Error was thrown. "Audits.checkContrast": Audits.checkContrastReturnValue; "Audits.checkFormsIssues": Audits.checkFormsIssuesReturnValue; "Extensions.loadUnpacked": Extensions.loadUnpackedReturnValue; + "Extensions.getStorageItems": Extensions.getStorageItemsReturnValue; + "Extensions.removeStorageItems": Extensions.removeStorageItemsReturnValue; + "Extensions.clearStorageItems": Extensions.clearStorageItemsReturnValue; + "Extensions.setStorageItems": Extensions.setStorageItemsReturnValue; "Autofill.trigger": Autofill.triggerReturnValue; "Autofill.setAddresses": Autofill.setAddressesReturnValue; "Autofill.disable": Autofill.disableReturnValue; @@ -20832,6 +21027,7 @@ Error was thrown. "DOM.setNodeStackTracesEnabled": DOM.setNodeStackTracesEnabledReturnValue; "DOM.getNodeStackTraces": DOM.getNodeStackTracesReturnValue; "DOM.getFileInfo": DOM.getFileInfoReturnValue; + "DOM.getDetachedDomNodes": DOM.getDetachedDomNodesReturnValue; "DOM.setInspectedNode": DOM.setInspectedNodeReturnValue; "DOM.setNodeName": DOM.setNodeNameReturnValue; "DOM.setNodeValue": DOM.setNodeValueReturnValue; @@ -21216,6 +21412,10 @@ Error was thrown. "PWA.launchFilesInApp": PWA.launchFilesInAppReturnValue; "PWA.openCurrentPageInApp": PWA.openCurrentPageInAppReturnValue; "PWA.changeAppUserSettings": PWA.changeAppUserSettingsReturnValue; + "BluetoothEmulation.enable": BluetoothEmulation.enableReturnValue; + "BluetoothEmulation.disable": BluetoothEmulation.disableReturnValue; + "BluetoothEmulation.simulatePreconnectedPeripheral": BluetoothEmulation.simulatePreconnectedPeripheralReturnValue; + "BluetoothEmulation.simulateAdvertisement": BluetoothEmulation.simulateAdvertisementReturnValue; "Console.clearMessages": Console.clearMessagesReturnValue; "Console.disable": Console.disableReturnValue; "Console.enable": Console.enableReturnValue; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 9c125fef1e..37281a1eb4 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -288,41 +288,8 @@ export interface Page { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. - * - * **Bundling** - * - * If you have a complex script split into several files, it needs to be bundled into a single file first. We - * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a - * commonjs module and pass `path` and `arg`. - * - * ```js - * // mocks/mockRandom.ts - * // This script can import other files. - * import { defaultValue } from './defaultValue'; - * - * export default function(value?: number) { - * window.Math.random = () => value ?? defaultValue; - * } - * ``` - * - * ```js - * // tests/example.spec.ts - * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - * - * // Passing 42 as an argument to the default export function. - * await page.addInitScript({ path: mockPath }, 42); - * - * // Make sure to pass something even if you do not need to pass an argument. - * // This instructs Playwright to treat the file as a commonjs module. - * await page.addInitScript({ path: mockPath }, ''); - * ``` - * * @param script Script to be evaluated in the page. - * @param arg Optional JSON-serializable argument to pass to `script`. - * - When `script` is a function, the argument is passed to it directly. - * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either - * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this - * argument. + * @param arg Optional argument to pass to `script` (only supported when passing a function). */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; @@ -898,17 +865,55 @@ export interface Page { exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. + * + * **Usage** + * + * ```js + * page.on('request', async request => { + * const response = await request.response(); + * const body = await response.body(); + * console.log(body.byteLength); + * }); + * await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); + * // Waits for all the reported 'request' events to resolve. + * await page.removeAllListeners('request', { behavior: 'wait' }); + * ``` + * * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. + * + * **Usage** + * + * ```js + * page.on('request', async request => { + * const response = await request.response(); + * const body = await response.body(); + * console.log(body.byteLength); + * }); + * await page.goto('https://playwright.dev', { waitUntil: 'domcontentloaded' }); + * // Waits for all the reported 'request' events to resolve. + * await page.removeAllListeners('request', { behavior: 'wait' }); + * ``` + * * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * Emitted when the page closes. */ @@ -3897,10 +3902,8 @@ export interface Page { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -7023,10 +7026,8 @@ export interface Frame { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -7571,9 +7572,9 @@ export interface Frame { * If a page opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's browser * context. * - * Playwright allows creating "incognito" browser contexts with + * Playwright allows creating isolated non-persistent browser contexts with * [browser.newContext([options])](https://playwright.dev/docs/api/class-browser#browser-new-context) method. - * "Incognito" browser contexts don't write any browsing data to disk. + * Non-persistent browser contexts don't write any browsing data to disk. * * ```js * // Create a new incognito browser context @@ -7699,56 +7700,33 @@ export interface BrowserContext { * [browserContext.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-browsercontext#browser-context-add-init-script) * and [page.addInitScript(script[, arg])](https://playwright.dev/docs/api/class-page#page-add-init-script) is not * defined. - * - * **Bundling** - * - * If you have a complex script split into several files, it needs to be bundled into a single file first. We - * recommend running [`esbuild`](https://esbuild.github.io/) or [`webpack`](https://webpack.js.org/) to produce a - * commonjs module and pass `path` and `arg`. - * - * ```js - * // mocks/mockRandom.ts - * // This script can import other files. - * import { defaultValue } from './defaultValue'; - * - * export default function(value?: number) { - * window.Math.random = () => value ?? defaultValue; - * } - * ``` - * - * ```js - * // tests/example.spec.ts - * const mockPath = { path: path.resolve(__dirname, '../mocks/mockRandom.js') }; - * - * // Passing 42 as an argument to the default export function. - * await context.addInitScript({ path: mockPath }, 42); - * - * // Make sure to pass something even if you do not need to pass an argument. - * // This instructs Playwright to treat the file as a commonjs module. - * await context.addInitScript({ path: mockPath }, ''); - * ``` - * * @param script Script to be evaluated in all pages in the browser context. - * @param arg Optional JSON-serializable argument to pass to `script`. - * - When `script` is a function, the argument is passed to it directly. - * - When `script` is a file path, the file is assumed to be a commonjs module. The default export, either - * `module.exports` or `module.exports.default`, should be a function that's going to be executed with this - * argument. + * @param arg Optional argument to pass to `script` (only supported when passing a function). */ addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * **NOTE** Only works with Chromium browser's persistent context. * @@ -9022,17 +9000,27 @@ export interface BrowserContext { */ export interface Browser { /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ removeAllListeners(type?: string): this; /** - * Removes all the listeners of the given type if the type is given. Otherwise removes all the listeners. + * Removes all the listeners of the given type (or all registered listeners if no type given). Allows to wait for + * async listeners to complete or to ignore subsequent errors from these listeners. * @param type * @param options */ - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; /** * Emitted when Browser gets disconnected from the browser application. This might happen because of one of the * following: @@ -11136,10 +11124,8 @@ export interface ElementHandle extends JSHandle { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -13331,10 +13317,8 @@ export interface Locator { force?: boolean; /** - * Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You - * can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as - * navigating to inaccessible pages. Defaults to `false`. - * @deprecated This option will default to `true` in the future. + * This option has no effect. + * @deprecated This option has no effect. */ noWaitAfter?: boolean; @@ -17336,8 +17320,8 @@ export interface APIResponse { headers(): { [key: string]: string; }; /** - * An array with all the request HTTP headers associated with this response. Header names are not lower-cased. Headers - * with multiple entries, such as `Set-Cookie`, appear in the array multiple times. + * An array with all the response HTTP headers associated with this response. Header names are not lower-cased. + * Headers with multiple entries, such as `Set-Cookie`, appear in the array multiple times. */ headersArray(): Array<{ /** diff --git a/packages/playwright-ct-svelte/package.json b/packages/playwright-ct-svelte/package.json index 3f287315c2..88cf44d6ee 100644 --- a/packages/playwright-ct-svelte/package.json +++ b/packages/playwright-ct-svelte/package.json @@ -34,7 +34,7 @@ "@sveltejs/vite-plugin-svelte": "^3.0.1" }, "devDependencies": { - "svelte": "^4.2.8" + "svelte": "^4.2.19" }, "bin": { "playwright": "cli.js" diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 0460ae56d9..28f82688dc 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -44,7 +44,7 @@ export interface TestServerInterface { installBrowsers(params: {}): Promise; - runGlobalSetup(params: {}): Promise<{ + runGlobalSetup(params: { outputDir?: string }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] }>; @@ -81,6 +81,7 @@ export interface TestServerInterface { locations?: string[]; grep?: string; grepInvert?: string; + outputDir?: string; }): Promise<{ report: ReportEntry[], status: reporterTypes.FullResult['status'] diff --git a/packages/playwright/src/matchers/expect.ts b/packages/playwright/src/matchers/expect.ts index 3e49360eb7..ea796bfc72 100644 --- a/packages/playwright/src/matchers/expect.ts +++ b/packages/playwright/src/matchers/expect.ts @@ -60,7 +60,7 @@ import { } from '../common/expectBundle'; import { zones } from 'playwright-core/lib/utils'; import { TestInfoImpl } from '../worker/testInfo'; -import { ExpectError } from './matcherHint'; +import { ExpectError, isExpectError } from './matcherHint'; // #region // Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts @@ -289,8 +289,8 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler { const step = testInfo._addStep(stepInfo); - const reportStepError = (jestError: ExpectError) => { - const error = new ExpectError(jestError, customMessage, stackFrames); + const reportStepError = (jestError: Error | unknown) => { + const error = isExpectError(jestError) ? new ExpectError(jestError, customMessage, stackFrames) : jestError; step.complete({ error }); if (this._info.isSoft) testInfo._failWithError(error); diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index e8aba2bbff..8a78932c68 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -64,3 +64,7 @@ export class ExpectError extends Error { this.stack = this.name + ': ' + this.message + '\n' + stringifyStackFrames(stackFrames).join('\n'); } } + +export function isExpectError(e: unknown): e is ExpectError { + return e instanceof Error && 'matcherResult' in e; +} diff --git a/packages/playwright/src/matchers/matchers.ts b/packages/playwright/src/matchers/matchers.ts index 08ae8b6385..3ca9180ae2 100644 --- a/packages/playwright/src/matchers/matchers.ts +++ b/packages/playwright/src/matchers/matchers.ts @@ -20,8 +20,8 @@ import { colors } from 'playwright-core/lib/utilsBundle'; import { expectTypes, callLogText } from '../util'; import { toBeTruthy } from './toBeTruthy'; import { toEqual } from './toEqual'; -import { toExpectedTextValues, toMatchText } from './toMatchText'; -import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline } from 'playwright-core/lib/utils'; +import { toMatchText } from './toMatchText'; +import { constructURLBasedOnBaseURL, isRegExp, isString, isTextualMimeType, pollAgainstDeadline, serializeExpectedTextValues } from 'playwright-core/lib/utils'; import { currentTestInfo } from '../common/globals'; import { TestInfoImpl } from '../worker/testInfo'; import type { ExpectMatcherState } from '../../types/test'; @@ -163,12 +163,12 @@ export function toContainText( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues(expected, { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.contain.text.array', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, { ...options, contains: true }); } else { return toMatchText.call(this, 'toContainText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { matchSubstring: true, normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options.useInnerText, timeout }); }, expected, options); } @@ -181,7 +181,7 @@ export function toHaveAccessibleDescription( options?: { timeout?: number, ignoreCase?: boolean }, ) { return toMatchText.call(this, 'toHaveAccessibleDescription', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.accessible.description', { expectedText, isNot, timeout }); }, expected, options); } @@ -193,7 +193,7 @@ export function toHaveAccessibleName( options?: { timeout?: number, ignoreCase?: boolean }, ) { return toMatchText.call(this, 'toHaveAccessibleName', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.accessible.name', { expectedText, isNot, timeout }); }, expected, options); } @@ -218,7 +218,7 @@ export function toHaveAttribute( }, options); } return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected as (string | RegExp)], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.attribute.value', { expressionArg: name, expectedText, isNot, timeout }); }, expected as (string | RegExp), options); } @@ -231,12 +231,12 @@ export function toHaveClass( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected); + const expectedText = serializeExpectedTextValues(expected); return await locator._expect('to.have.class.array', { expectedText, isNot, timeout }); }, expected, options); } else { return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.class', { expectedText, isNot, timeout }); }, expected, options); } @@ -261,7 +261,7 @@ export function toHaveCSS( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.css', { expressionArg: name, expectedText, isNot, timeout }); }, expected, options); } @@ -273,7 +273,7 @@ export function toHaveId( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.id', { expectedText, isNot, timeout }); }, expected, options); } @@ -299,7 +299,7 @@ export function toHaveRole( if (!isString(expected)) throw new Error(`"role" argument in toHaveRole must be a string`); return toMatchText.call(this, 'toHaveRole', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.role', { expectedText, isNot, timeout }); }, expected, options); } @@ -312,12 +312,12 @@ export function toHaveText( ) { if (Array.isArray(expected)) { return toEqual.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues(expected, { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text.array', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } else { return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true, ignoreCase: options.ignoreCase }); return await locator._expect('to.have.text', { expectedText, isNot, useInnerText: options?.useInnerText, timeout }); }, expected, options); } @@ -330,7 +330,7 @@ export function toHaveValue( options?: { timeout?: number }, ) { return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected]); + const expectedText = serializeExpectedTextValues([expected]); return await locator._expect('to.have.value', { expectedText, isNot, timeout }); }, expected, options); } @@ -342,7 +342,7 @@ export function toHaveValues( options?: { timeout?: number }, ) { return toEqual.call(this, 'toHaveValues', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues(expected); + const expectedText = serializeExpectedTextValues(expected); return await locator._expect('to.have.values', { expectedText, isNot, timeout }); }, expected, options); } @@ -355,7 +355,7 @@ export function toHaveTitle( ) { const locator = page.locator(':root') as LocatorEx; return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { normalizeWhiteSpace: true }); + const expectedText = serializeExpectedTextValues([expected], { normalizeWhiteSpace: true }); return await locator._expect('to.have.title', { expectedText, isNot, timeout }); }, expected, options); } @@ -370,7 +370,7 @@ export function toHaveURL( expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected; const locator = page.locator(':root') as LocatorEx; return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (isNot, timeout) => { - const expectedText = toExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); + const expectedText = serializeExpectedTextValues([expected], { ignoreCase: options?.ignoreCase }); return await locator._expect('to.have.url', { expectedText, isNot, timeout }); }, expected, options); } diff --git a/packages/playwright/src/matchers/toMatchText.ts b/packages/playwright/src/matchers/toMatchText.ts index 790b402d2f..ebac8f8028 100644 --- a/packages/playwright/src/matchers/toMatchText.ts +++ b/packages/playwright/src/matchers/toMatchText.ts @@ -15,8 +15,6 @@ */ -import type { ExpectedTextValue } from '@protocol/channels'; -import { isRegExp, isString } from 'playwright-core/lib/utils'; import { expectTypes, callLogText } from '../util'; import { printReceivedStringContainExpectedResult, @@ -95,14 +93,3 @@ export async function toMatchText( timeout: timedOut ? timeout : undefined, }; } - -export function toExpectedTextValues(items: (string | RegExp)[], options: { matchSubstring?: boolean, normalizeWhiteSpace?: boolean, ignoreCase?: boolean } = {}): ExpectedTextValue[] { - return items.map(i => ({ - string: isString(i) ? i : undefined, - regexSource: isRegExp(i) ? i.source : undefined, - regexFlags: isRegExp(i) ? i.flags : undefined, - matchSubstring: options.matchSubstring, - ignoreCase: options.ignoreCase, - normalizeWhiteSpace: options.normalizeWhiteSpace, - })); -} diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 2aa513b90f..1df2d72e8c 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -148,7 +148,10 @@ export class TestServerDispatcher implements TestServerInterface { async runGlobalSetup(params: Parameters[0]): ReturnType { await this.runGlobalTeardown(); - const { config, error } = await this._loadConfig(); + const overrides: ConfigCLIOverrides = { + outputDir: params.outputDir, + }; + const { config, error } = await this._loadConfig(overrides); if (!config) { const { reporter, report } = await this._collectingInternalReporter(); // Produce dummy config when it has an error. @@ -256,6 +259,7 @@ export class TestServerDispatcher implements TestServerInterface { const overrides: ConfigCLIOverrides = { repeatEach: 1, retries: 0, + outputDir: params.outputDir, }; const { config, error } = await this._loadConfig(overrides); if (!config) { diff --git a/packages/playwright/src/runner/vcs.ts b/packages/playwright/src/runner/vcs.ts index 707d820ed5..6f7ed55c9a 100644 --- a/packages/playwright/src/runner/vcs.ts +++ b/packages/playwright/src/runner/vcs.ts @@ -30,7 +30,7 @@ export async function detectChangedTestFiles(baseCommit: string, configDir: stri const unknownRevision = error.output.some(line => line?.includes('unknown revision')); if (unknownRevision) { - const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe' }).trim() === 'true'; + const isShallowClone = childProcess.execSync('git rev-parse --is-shallow-repository', { encoding: 'utf-8', stdio: 'pipe', cwd: configDir }).trim() === 'true'; if (isShallowClone) { throw new Error([ `The repository is a shallow clone and does not have '${baseCommit}' available locally.`, diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index e6b07be7c0..378b32524f 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -30,7 +30,7 @@ import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; export interface TestStepInternal { - complete(result: { error?: Error, attachments?: Attachment[] }): void; + complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; stepId: string; title: string; category: 'hook' | 'fixture' | 'test.step' | 'expect' | 'attach' | string; @@ -270,7 +270,7 @@ export class TestInfoImpl implements TestInfo { step.endWallTime = Date.now(); if (result.error) { - if (!(result.error as any)[stepSymbol]) + if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) (result.error as any)[stepSymbol] = step; const error = serializeError(result.error); if (data.boxedStack) @@ -327,13 +327,13 @@ export class TestInfoImpl implements TestInfo { this.status = 'interrupted'; } - _failWithError(error: Error) { + _failWithError(error: Error | unknown) { if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; const serialized = serializeError(error); - const step = (error as any)[stepSymbol] as TestStepInternal | undefined; + const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; if (step && step.boxedStack) - serialized.stack = `${error.name}: ${error.message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; + serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; this.errors.push(serialized); this._tracing.appendForError(serialized); } diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 0131cb16c9..2cf21f7350 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -1825,8 +1825,10 @@ type TestDetailsAnnotation = { description?: string; }; +type TestDetailsTag = `@${string}`; + export type TestDetails = { - tag?: string | string[]; + tag?: TestDetailsTag | TestDetailsTag[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } @@ -6565,15 +6567,17 @@ type MakeMatchers = { rejects: MakeMatchers, any, ExtendedMatchers>; } & IfAny, SpecificMatchers & ToUserMatcherObject>; +type PollMatchers = { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: PollMatchers; +} & BaseMatchers & ToUserMatcherObject; + export type Expect = { (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers, T> & { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: BaseMatchers, T>; - }; + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; configure: (configuration: { message?: string, diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index f3e0a2c35a..d0f5d43f79 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1753,7 +1753,6 @@ export type BrowserContextRecorderSupplementEnableParams = { device?: string, saveStorage?: string, outputFile?: string, - handleSIGINT?: boolean, omitCallTracking?: boolean, }; export type BrowserContextRecorderSupplementEnableOptions = { @@ -1766,7 +1765,6 @@ export type BrowserContextRecorderSupplementEnableOptions = { device?: string, saveStorage?: string, outputFile?: string, - handleSIGINT?: boolean, omitCallTracking?: boolean, }; export type BrowserContextRecorderSupplementEnableResult = void; @@ -2939,7 +2937,6 @@ export type FrameSelectOptionParams = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type FrameSelectOptionOptions = { strict?: boolean, @@ -2952,7 +2949,6 @@ export type FrameSelectOptionOptions = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type FrameSelectOptionResult = { values: string[], @@ -3555,7 +3551,6 @@ export type ElementHandleSelectOptionParams = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type ElementHandleSelectOptionOptions = { elements?: ElementHandleChannel[], @@ -3567,7 +3562,6 @@ export type ElementHandleSelectOptionOptions = { }[], force?: boolean, timeout?: number, - noWaitAfter?: boolean, }; export type ElementHandleSelectOptionResult = { values: string[], diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 4c25212c57..d7c33b05d8 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1189,7 +1189,6 @@ BrowserContext: device: string? saveStorage: string? outputFile: string? - handleSIGINT: boolean? omitCallTracking: boolean? newCDPSession: @@ -2185,7 +2184,6 @@ Frame: index: number? force: boolean? timeout: number? - noWaitAfter: boolean? returns: values: type: array @@ -2741,7 +2739,6 @@ ElementHandle: index: number? force: boolean? timeout: number? - noWaitAfter: boolean? returns: values: type: array diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index 002b7b7fb9..03a2e936df 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -59,7 +59,7 @@ const RequestTab: React.FunctionComponent<{ React.useEffect(() => { const readResources = async () => { if (resource.request.postData) { - const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type'); + const requestContentTypeHeader = resource.request.headers.find(q => q.name.toLowerCase() === 'content-type'); const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; if (resource.request.postData._sha1) { const response = await fetch(`sha1/${resource.request.postData._sha1}`); diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 0f799b2035..b88aebe6fc 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -205,12 +205,14 @@ export const UIModeView: React.FC<{}> = ({ interceptStdio: true, watchTestDirs: true }); - const { status, report } = await testServerConnection.runGlobalSetup({}); + const { status, report } = await testServerConnection.runGlobalSetup({ + outputDir: queryParams.outputDir, + }); teleSuiteUpdater.processGlobalReport(report); if (status !== 'passed') return; - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); teleSuiteUpdater.processListReport(result.report); testServerConnection.onReport(params => { @@ -333,7 +335,7 @@ export const UIModeView: React.FC<{}> = ({ commandQueue.current = commandQueue.current.then(async () => { setIsLoading(true); try { - const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert }); + const result = await testServerConnection.listTests({ projects: queryParams.projects, locations: queryParams.args, grep: queryParams.grep, grepInvert: queryParams.grepInvert, outputDir: queryParams.outputDir }); teleSuiteUpdater.processListReport(result.report); } catch (e) { // eslint-disable-next-line no-console diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 01ae6142bd..2d6b0bd8bd 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -83,7 +83,7 @@ export const Workbench: React.FunctionComponent<{ setRevealedStack(action?.stack); }, [setSelectedActionImpl, setRevealedStack]); - const sources = React.useMemo(() => model?.sources || new Map(), [model]); + const sources = React.useMemo(() => model?.sources || new Map(), [model]); React.useEffect(() => { setSelectedTime(undefined); @@ -179,9 +179,17 @@ export const Workbench: React.FunctionComponent<{ selectPropertiesTab('source'); }} /> }; + + // Fallback location w/o action stands for file / test. + // Render error count on Source tab for that case. + let fallbackSourceErrorCount: number | undefined = undefined; + if (!selectedAction && fallbackLocation) + fallbackSourceErrorCount = fallbackLocation.source?.errors.length; + const sourceTab: TabbedPaneTabModel = { id: 'source', title: 'Source', + errorCount: fallbackSourceErrorCount, render: () => - - - Detect Touch Test - - - - - - diff --git a/tests/assets/injectedmodule.js b/tests/assets/injectedmodule.js deleted file mode 100644 index bc099f243f..0000000000 --- a/tests/assets/injectedmodule.js +++ /dev/null @@ -1,33 +0,0 @@ -"use strict"; -var __defProp = Object.defineProperty; -var __getOwnPropDesc = Object.getOwnPropertyDescriptor; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __hasOwnProp = Object.prototype.hasOwnProperty; -var __export = (target, all) => { - for (var name in all) - __defProp(target, name, { get: all[name], enumerable: true }); -}; -var __copyProps = (to, from, except, desc) => { - if (from && typeof from === "object" || typeof from === "function") { - for (let key of __getOwnPropNames(from)) - if (!__hasOwnProp.call(to, key) && key !== except) - __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); - } - return to; -}; -var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); - -// one.ts -var one_exports = {}; -__export(one_exports, { - default: () => one_default -}); -module.exports = __toCommonJS(one_exports); - -// two.ts -var value = 42; - -// one.ts -function one_default(arg) { - window.injected = arg ?? value; -} diff --git a/tests/assets/modernizr.html b/tests/assets/modernizr.html deleted file mode 100644 index 5096ffba74..0000000000 --- a/tests/assets/modernizr.html +++ /dev/null @@ -1,21 +0,0 @@ - - - diff --git a/tests/assets/modernizr.js b/tests/assets/modernizr.js deleted file mode 100644 index 1a03ac53b5..0000000000 --- a/tests/assets/modernizr.js +++ /dev/null @@ -1,5 +0,0 @@ -/*! modernizr 3.6.0 (Custom Build) | MIT * - * https://modernizr.com/download/?-MessageChannel-adownload-ambientlight-animation-apng-appearance-applicationcache-arrow-atobbtoa-audio-audioloop-audiopreload-backdropfilter-backgroundblendmode-backgroundcliptext-backgroundsize-batteryapi-bdi-beacon-bgpositionshorthand-bgpositionxy-bgrepeatspace_bgrepeatround-bgsizecover-blobconstructor-bloburls-blobworkers-borderimage-borderradius-boxshadow-boxsizing-canvas-canvasblending-canvastext-canvaswinding-capture-checked-classlist-contains-contenteditable-contextmenu-cookies-cors-createelementattrs_createelement_attrs-cryptography-cssall-cssanimations-csscalc-csschunit-csscolumns-cssescape-cssexunit-cssfilters-cssgradients-cssgrid_cssgridlegacy-csshyphens_softhyphens_softhyphensfind-cssinvalid-cssmask-csspointerevents-csspositionsticky-csspseudoanimations-csspseudotransitions-cssreflections-cssremunit-cssresize-cssscrollbar-csstransforms-csstransforms3d-csstransformslevel2-csstransitions-cssvalid-cssvhunit-cssvmaxunit-cssvminunit-cssvwunit-cubicbezierrange-customelements-customevent-customprotocolhandler-dart-datachannel-datalistelem-dataset-datauri-dataview-dataworkers-details-devicemotion_deviceorientation-directory-display_runin-displaytable-documentfragment-ellipsis-emoji-es5-es5array-es5date-es5function-es5object-es5string-es5syntax-es5undefined-es6array-es6collections-es6math-es6number-es6object-es6string-eventlistener-eventsource-exiforientation-fetch-fileinput-filereader-filesystem-flash-flexbox-flexboxlegacy-flexboxtweener-flexwrap-focuswithin-fontface-forcetouch-formattribute-formvalidation-framed-fullscreen-gamepads-generatedcontent-generators-geolocation-getrandomvalues-getusermedia-hairline-hashchange-hidden-hiddenscroll-history-hovermq-hsla-htmlimports-ie8compat-imgcrossorigin-indexeddb-indexeddbblob-inlinesvg-input-inputformaction-inputformenctype-inputformmethod-inputformtarget-inputtypes-intl-jpeg2000-jpegxr-json-lastchild-ligatures-localizednumber-localstorage-lowbandwidth-lowbattery-matchmedia-mathml-mediaqueries-microdata-multiplebgs-mutationobserver-notification-nthchild-objectfit-olreversed-oninput-opacity-outputelem-overflowscrolling-pagevisibility-passiveeventlisteners-peerconnection-performance-picture-placeholder-pointerevents-pointerlock-pointermq-postmessage-preserve3d-progressbar_meter-promises-proximity-queryselector-quotamanagement-regions-requestanimationframe-requestautocomplete-rgba-ruby-sandbox-scriptasync-scriptdefer-scrollsnappoints-seamless-search-serviceworker-sessionstorage-shapes-sharedworkers-siblinggeneral-sizes-smil-speechrecognition-speechsynthesis-srcdoc-srcset-strictmode-stylescoped-subpixelfont-supports-svg-svgasimg-svgclippaths-svgfilters-svgforeignobject-target-template-templatestrings-textalignlast-textareamaxlength-textshadow-texttrackapi_track-time-todataurljpeg_todataurlpng_todataurlwebp-touchevents-transferables-typedarrays-unicode-unicoderange-unknownelements-urlparser-urlsearchparams-userdata-userselect-variablefonts-vibrate-video-videoautoplay-videocrossorigin-videoloop-videopreload-vml-webaudio-webgl-webglextensions-webintents-webp-webpalpha-webpanimation-webplossless_webp_lossless-websockets-websocketsbinary-websqldatabase-webworkers-willchange-wrapflow-xdomainrequest-xhr2-xhrresponsetype-xhrresponsetypearraybuffer-xhrresponsetypeblob-xhrresponsetypedocument-xhrresponsetypejson-xhrresponsetypetext-setclasses !*/ -!function(window,document,undefined){function is(A,e){return typeof A===e}function testRunner(){var A,e,t,n,r,o,i;for(var d in tests)if(tests.hasOwnProperty(d)){if(A=[],e=tests[d],e.name&&(A.push(e.name.toLowerCase()),e.options&&e.options.aliases&&e.options.aliases.length))for(t=0;td;d++)if(s=A[d],l=mStyle.style[s],contains(s,"-")&&(s=cssToDOM(s)),mStyle.style[s]!==undefined){if(n||is(t,"undefined"))return r(),"pfx"==e?s:!0;try{mStyle.style[s]=t}catch(u){}if(mStyle.style[s]!=l)return r(),"pfx"==e?s:!0}return r(),!1}function testPropsAll(A,e,t,n,r){var o=A.charAt(0).toUpperCase()+A.slice(1),i=(A+" "+cssomPrefixes.join(o+" ")+o).split(" ");return is(e,"string")||is(e,"undefined")?testProps(i,e,n,r):(i=(A+" "+domPrefixes.join(o+" ")+o).split(" "),testDOMProps(i,e,t))}function detectDeleteDatabase(A,e){var t=A.deleteDatabase(e);t.onsuccess=function(){addTest("indexeddb.deletedatabase",!0)},t.onerror=function(){addTest("indexeddb.deletedatabase",!1)}}function testAllProps(A,e,t){return testPropsAll(A,undefined,undefined,e,t)}var classes=[],tests=[],ModernizrProto={_version:"3.6.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(A,e){var t=this;setTimeout(function(){e(t[A])},0)},addTest:function(A,e,t){tests.push({name:A,fn:e,options:t})},addAsyncTest:function(A){tests.push({name:null,fn:A})}},Modernizr=function(){};Modernizr.prototype=ModernizrProto,Modernizr=new Modernizr,Modernizr.addTest("history",function(){var A=navigator.userAgent;return-1===A.indexOf("Android 2.")&&-1===A.indexOf("Android 4.0")||-1===A.indexOf("Mobile Safari")||-1!==A.indexOf("Chrome")||-1!==A.indexOf("Windows Phone")||"file:"===location.protocol?window.history&&"pushState"in window.history:!1}),Modernizr.addTest("ie8compat",!window.addEventListener&&!!document.documentMode&&7===document.documentMode),Modernizr.addTest("applicationcache","applicationCache"in window),Modernizr.addTest("blobconstructor",function(){try{return!!new Blob}catch(A){return!1}},{aliases:["blob-constructor"]}),Modernizr.addTest("cookies",function(){try{document.cookie="cookietest=1";var A=-1!=document.cookie.indexOf("cookietest=");return document.cookie="cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT",A}catch(e){return!1}}),Modernizr.addTest("cors","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest),Modernizr.addTest("customelements","customElements"in window),Modernizr.addTest("customprotocolhandler",function(){if(!navigator.registerProtocolHandler)return!1;try{navigator.registerProtocolHandler("thisShouldFail")}catch(A){return A instanceof TypeError}return!1}),Modernizr.addTest("customevent","CustomEvent"in window&&"function"==typeof window.CustomEvent),Modernizr.addTest("dataview","undefined"!=typeof DataView&&"getFloat64"in DataView.prototype),Modernizr.addTest("eventlistener","addEventListener"in window),Modernizr.addTest("geolocation","geolocation"in navigator),Modernizr.addTest("json","JSON"in window&&"parse"in JSON&&"stringify"in JSON),Modernizr.addTest("messagechannel","MessageChannel"in window),Modernizr.addTest("notification",function(){if(!window.Notification||!window.Notification.requestPermission)return!1;if("granted"===window.Notification.permission)return!0;try{new window.Notification("")}catch(A){if("TypeError"===A.name)return!1}return!0}),Modernizr.addTest("postmessage","postMessage"in window),Modernizr.addTest("queryselector","querySelector"in document&&"querySelectorAll"in document),Modernizr.addTest("serviceworker","serviceWorker"in navigator),Modernizr.addTest("svg",!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect),Modernizr.addTest("templatestrings",function(){var supports;try{eval("``"),supports=!0}catch(e){}return!!supports}),Modernizr.addTest("typedarrays","ArrayBuffer"in window);var supports=!1;try{supports="WebSocket"in window&&2===window.WebSocket.CLOSING}catch(e){}Modernizr.addTest("websockets",supports),Modernizr.addTest("xdomainrequest","XDomainRequest"in window),Modernizr.addTest("webaudio",function(){var A="webkitAudioContext"in window,e="AudioContext"in window;return Modernizr._config.usePrefixes?A||e:e});var CSS=window.CSS;Modernizr.addTest("cssescape",CSS?"function"==typeof CSS.escape:!1),Modernizr.addTest("focuswithin",function(){try{document.querySelector(":focus-within")}catch(A){return!1}return!0});var newSyntax="CSS"in window&&"supports"in window.CSS,oldSyntax="supportsCSS"in window;Modernizr.addTest("supports",newSyntax||oldSyntax),Modernizr.addTest("target",function(){var A=window.document;if(!("querySelectorAll"in A))return!1;try{return A.querySelectorAll(":target"),!0}catch(e){return!1}}),Modernizr.addTest("microdata","getItems"in document),Modernizr.addTest("mutationobserver",!!window.MutationObserver||!!window.WebKitMutationObserver),Modernizr.addTest("passiveeventlisteners",function(){var A=!1;try{var e=Object.defineProperty({},"passive",{get:function(){A=!0}});window.addEventListener("test",null,e)}catch(t){}return A}),Modernizr.addTest("picture","HTMLPictureElement"in window),Modernizr.addTest("es5array",function(){return!!(Array.prototype&&Array.prototype.every&&Array.prototype.filter&&Array.prototype.forEach&&Array.prototype.indexOf&&Array.prototype.lastIndexOf&&Array.prototype.map&&Array.prototype.some&&Array.prototype.reduce&&Array.prototype.reduceRight&&Array.isArray)}),Modernizr.addTest("es5date",function(){var A="2013-04-12T06:06:37.307Z",e=!1;try{e=!!Date.parse(A)}catch(t){}return!!(Date.now&&Date.prototype&&Date.prototype.toISOString&&Date.prototype.toJSON&&e)}),Modernizr.addTest("es5function",function(){return!(!Function.prototype||!Function.prototype.bind)}),Modernizr.addTest("beacon","sendBeacon"in navigator),Modernizr.addTest("lowbandwidth",function(){var A=navigator.connection||{type:0};return 3==A.type||4==A.type||/^[23]g$/.test(A.type)}),Modernizr.addTest("eventsource","EventSource"in window),Modernizr.addTest("fetch","fetch"in window),Modernizr.addTest("xhrresponsetype",function(){if("undefined"==typeof XMLHttpRequest)return!1;var A=new XMLHttpRequest;return A.open("get","/",!0),"response"in A}()),Modernizr.addTest("xhr2","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest),Modernizr.addTest("speechsynthesis","SpeechSynthesisUtterance"in window),Modernizr.addTest("localstorage",function(){var A="modernizr";try{return localStorage.setItem(A,A),localStorage.removeItem(A),!0}catch(e){return!1}}),Modernizr.addTest("sessionstorage",function(){var A="modernizr";try{return sessionStorage.setItem(A,A),sessionStorage.removeItem(A),!0}catch(e){return!1}}),Modernizr.addTest("websqldatabase","openDatabase"in window),Modernizr.addTest("es5object",function(){return!!(Object.keys&&Object.create&&Object.getPrototypeOf&&Object.getOwnPropertyNames&&Object.isSealed&&Object.isFrozen&&Object.isExtensible&&Object.getOwnPropertyDescriptor&&Object.defineProperty&&Object.defineProperties&&Object.seal&&Object.freeze&&Object.preventExtensions)}),Modernizr.addTest("svgfilters",function(){var A=!1;try{A="SVGFEColorMatrixElement"in window&&2==SVGFEColorMatrixElement.SVG_FECOLORMATRIX_TYPE_SATURATE}catch(e){}return A}),Modernizr.addTest("strictmode",function(){"use strict";return!this}()),Modernizr.addTest("es5string",function(){return!(!String.prototype||!String.prototype.trim)}),Modernizr.addTest("es5syntax",function(){var value,obj,stringAccess,getter,setter,reservedWords,zeroWidthChars;try{return stringAccess=eval('"foobar"[3] === "b"'),getter=eval("({ get x(){ return 1 } }).x === 1"),eval("({ set x(v){ value = v; } }).x = 1"),setter=1===value,eval("obj = ({ if: 1 })"),reservedWords=1===obj["if"],zeroWidthChars=eval("_‌‍ = true"),stringAccess&&getter&&setter&&reservedWords&&zeroWidthChars}catch(ignore){return!1}}),Modernizr.addTest("es5undefined",function(){var A,e;try{e=window.undefined,window.undefined=12345,A="undefined"==typeof window.undefined,window.undefined=e}catch(t){return!1}return A}),Modernizr.addTest("es5",function(){return!!(Modernizr.es5array&&Modernizr.es5date&&Modernizr.es5function&&Modernizr.es5object&&Modernizr.strictmode&&Modernizr.es5string&&Modernizr.json&&Modernizr.es5syntax&&Modernizr.es5undefined)}),Modernizr.addTest("es6array",!!(Array.prototype&&Array.prototype.copyWithin&&Array.prototype.fill&&Array.prototype.find&&Array.prototype.findIndex&&Array.prototype.keys&&Array.prototype.entries&&Array.prototype.values&&Array.from&&Array.of)),Modernizr.addTest("arrow",function(){try{eval("()=>{}")}catch(e){return!1}return!0}),Modernizr.addTest("es6collections",!!(window.Map&&window.Set&&window.WeakMap&&window.WeakSet)),Modernizr.addTest("generators",function(){try{new Function("function* test() {}")()}catch(A){return!1}return!0}),Modernizr.addTest("es6math",!!(Math&&Math.clz32&&Math.cbrt&&Math.imul&&Math.sign&&Math.log10&&Math.log2&&Math.log1p&&Math.expm1&&Math.cosh&&Math.sinh&&Math.tanh&&Math.acosh&&Math.asinh&&Math.atanh&&Math.hypot&&Math.trunc&&Math.fround)),Modernizr.addTest("es6number",!!(Number.isFinite&&Number.isInteger&&Number.isSafeInteger&&Number.isNaN&&Number.parseInt&&Number.parseFloat&&Number.isInteger(Number.MAX_SAFE_INTEGER)&&Number.isInteger(Number.MIN_SAFE_INTEGER)&&Number.isFinite(Number.EPSILON))),Modernizr.addTest("es6object",!!(Object.assign&&Object.is&&Object.setPrototypeOf)),Modernizr.addTest("promises",function(){return"Promise"in window&&"resolve"in window.Promise&&"reject"in window.Promise&&"all"in window.Promise&&"race"in window.Promise&&function(){var A;return new window.Promise(function(e){A=e}),"function"==typeof A}()}),Modernizr.addTest("es6string",!!(String.fromCodePoint&&String.raw&&String.prototype.codePointAt&&String.prototype.repeat&&String.prototype.startsWith&&String.prototype.endsWith&&String.prototype.includes)),Modernizr.addTest("devicemotion","DeviceMotionEvent"in window),Modernizr.addTest("deviceorientation","DeviceOrientationEvent"in window),Modernizr.addTest("filereader",!!(window.File&&window.FileList&&window.FileReader)),Modernizr.addTest("urlparser",function(){var A;try{return A=new URL("http://modernizr.com/"),"http://modernizr.com/"===A.href}catch(e){return!1}}),Modernizr.addTest("urlsearchparams","URLSearchParams"in window),Modernizr.addTest("framed",window.location!=top.location),Modernizr.addTest("webworkers","Worker"in window);var docElement=document.documentElement;Modernizr.addTest("contextmenu","contextMenu"in docElement&&"HTMLMenuItemElement"in window),Modernizr.addTest("cssall","all"in docElement.style),Modernizr.addTest("willchange","willChange"in docElement.style),Modernizr.addTest("classlist","classList"in docElement),Modernizr.addTest("documentfragment",function(){return"createDocumentFragment"in document&&"appendChild"in docElement}),Modernizr.addTest("contains",is(String.prototype.contains,"function"));var isSVG="svg"===docElement.nodeName.toLowerCase();Modernizr.addTest("audio",function(){var A=createElement("audio"),e=!1;try{e=!!A.canPlayType,e&&(e=new Boolean(e),e.ogg=A.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),e.mp3=A.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/,""),e.opus=A.canPlayType('audio/ogg; codecs="opus"')||A.canPlayType('audio/webm; codecs="opus"').replace(/^no$/,""),e.wav=A.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),e.m4a=(A.canPlayType("audio/x-m4a;")||A.canPlayType("audio/aac;")).replace(/^no$/,""))}catch(t){}return e}),Modernizr.addTest("canvas",function(){var A=createElement("canvas");return!(!A.getContext||!A.getContext("2d"))}),Modernizr.addTest("canvastext",function(){return Modernizr.canvas===!1?!1:"function"==typeof createElement("canvas").getContext("2d").fillText}),Modernizr.addTest("contenteditable",function(){if("contentEditable"in docElement){var A=createElement("div");return A.contentEditable=!0,"true"===A.contentEditable}}),Modernizr.addTest("emoji",function(){if(!Modernizr.canvastext)return!1;var A=window.devicePixelRatio||1,e=12*A,t=createElement("canvas"),n=t.getContext("2d");return n.fillStyle="#f00",n.textBaseline="top",n.font="32px Arial",n.fillText("🐨",0,0),0!==n.getImageData(e,e,1,1).data[0]}),Modernizr.addTest("olreversed","reversed"in createElement("ol")),Modernizr.addTest("userdata",!!createElement("div").addBehavior),Modernizr.addTest("video",function(){var A=createElement("video"),e=!1;try{e=!!A.canPlayType,e&&(e=new Boolean(e),e.ogg=A.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),e.h264=A.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),e.webm=A.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,""),e.vp9=A.canPlayType('video/webm; codecs="vp9"').replace(/^no$/,""),e.hls=A.canPlayType('application/x-mpegURL; codecs="avc1.42E01E"').replace(/^no$/,""))}catch(t){}return e}),Modernizr.addTest("vml",function(){var A,e=createElement("div"),t=!1;return isSVG||(e.innerHTML='',A=e.firstChild,"style"in A&&(A.style.behavior="url(#default#VML)"),t=A?"object"==typeof A.adj:!0),t}),Modernizr.addTest("webanimations","animate"in createElement("div")),Modernizr.addTest("webgl",function(){var A=createElement("canvas"),e="probablySupportsContext"in A?"probablySupportsContext":"supportsContext";return e in A?A[e]("webgl")||A[e]("experimental-webgl"):"WebGLRenderingContext"in window}),Modernizr.addTest("adownload",!window.externalHost&&"download"in createElement("a")),Modernizr.addTest("audioloop","loop"in createElement("audio")),Modernizr.addTest("canvasblending",function(){if(Modernizr.canvas===!1)return!1;var A=createElement("canvas").getContext("2d");try{A.globalCompositeOperation="screen"}catch(e){}return"screen"===A.globalCompositeOperation});var canvas=createElement("canvas");Modernizr.addTest("todataurljpeg",function(){return!!Modernizr.canvas&&0===canvas.toDataURL("image/jpeg").indexOf("data:image/jpeg")}),Modernizr.addTest("todataurlpng",function(){return!!Modernizr.canvas&&0===canvas.toDataURL("image/png").indexOf("data:image/png")}),Modernizr.addTest("todataurlwebp",function(){var A=!1;try{A=!!Modernizr.canvas&&0===canvas.toDataURL("image/webp").indexOf("data:image/webp")}catch(e){}return A}),Modernizr.addTest("canvaswinding",function(){if(Modernizr.canvas===!1)return!1;var A=createElement("canvas").getContext("2d");return A.rect(0,0,10,10),A.rect(2,2,6,6),A.isPointInPath(5,5,"evenodd")===!1}),Modernizr.addTest("bgpositionshorthand",function(){var A=createElement("a"),e=A.style,t="right 10px bottom 10px";return e.cssText="background-position: "+t+";",e.backgroundPosition===t}),Modernizr.addTest("multiplebgs",function(){var A=createElement("a").style;return A.cssText="background:url(https://),url(https://),red url(https://)",/(url\s*\(.*?){3}/.test(A.background)}),Modernizr.addTest("csspointerevents",function(){var A=createElement("a").style;return A.cssText="pointer-events:auto","auto"===A.pointerEvents}),Modernizr.addTest("cssremunit",function(){var A=createElement("a").style;try{A.fontSize="3rem"}catch(e){}return/rem/.test(A.fontSize)}),Modernizr.addTest("rgba",function(){var A=createElement("a").style;return A.cssText="background-color:rgba(150,255,150,.5)",(""+A.backgroundColor).indexOf("rgba")>-1}),Modernizr.addTest("preserve3d",function(){var A,e,t=window.CSS,n=!1;return t&&t.supports&&t.supports("(transform-style: preserve-3d)")?!0:(A=createElement("a"),e=createElement("a"),A.style.cssText="display: block; transform-style: preserve-3d; transform-origin: right; transform: rotateY(40deg);",e.style.cssText="display: block; width: 9px; height: 1px; background: #000; transform-origin: right; transform: rotateY(40deg);",A.appendChild(e),docElement.appendChild(A),n=e.getBoundingClientRect(),docElement.removeChild(A),n=n.width&&n.width<4)}),Modernizr.addTest("createelementattrs",function(){try{return"test"==createElement('').getAttribute("name")}catch(A){return!1}},{aliases:["createelement-attrs"]}),Modernizr.addTest("dataset",function(){var A=createElement("div");return A.setAttribute("data-a-b","c"),!(!A.dataset||"c"!==A.dataset.aB)}),Modernizr.addTest("hidden","hidden"in createElement("a")),Modernizr.addTest("outputelem","value"in createElement("output")),Modernizr.addTest("progressbar",createElement("progress").max!==undefined),Modernizr.addTest("meter",createElement("meter").max!==undefined),Modernizr.addTest("ruby",function(){function A(A,e){var t;return window.getComputedStyle?t=document.defaultView.getComputedStyle(A,null).getPropertyValue(e):A.currentStyle&&(t=A.currentStyle[e]),t}function e(){docElement.removeChild(t),t=null,n=null,r=null}var t=createElement("ruby"),n=createElement("rt"),r=createElement("rp"),o="display",i="fontSize";return t.appendChild(r),t.appendChild(n),docElement.appendChild(t),"none"==A(r,o)||"ruby"==A(t,o)&&"ruby-text"==A(n,o)||"6pt"==A(r,i)&&"6pt"==A(n,i)?(e(),!0):(e(),!1)}),Modernizr.addTest("template","content"in createElement("template")),Modernizr.addTest("srcset","srcset"in createElement("img")),Modernizr.addTest("time","valueAsDate"in createElement("time")),Modernizr.addTest("texttrackapi","function"==typeof createElement("video").addTextTrack),Modernizr.addTest("track","kind"in createElement("track")),Modernizr.addTest("unknownelements",function(){var A=createElement("a");return A.innerHTML="",1===A.childNodes.length}),Modernizr.addTest("inputformaction",!!("formAction"in createElement("input")),{aliases:["input-formaction"]}),Modernizr.addTest("inputformenctype",!!("formEnctype"in createElement("input")),{aliases:["input-formenctype"]}),Modernizr.addTest("inputformmethod",!!("formMethod"in createElement("input"))),Modernizr.addTest("inputformtarget",!!("formtarget"in createElement("input")),{aliases:["input-formtarget"]}),Modernizr.addTest("scriptasync","async"in createElement("script")),Modernizr.addTest("scriptdefer","defer"in createElement("script")),Modernizr.addTest("stylescoped","scoped"in createElement("style")),Modernizr.addTest("capture","capture"in createElement("input")),Modernizr.addTest("fileinput",function(){if(navigator.userAgent.match(/(Android (1.0|1.1|1.5|1.6|2.0|2.1))|(Windows Phone (OS 7|8.0))|(XBLWP)|(ZuneWP)|(w(eb)?OSBrowser)|(webOS)|(Kindle\/(1.0|2.0|2.5|3.0))/))return!1;var A=createElement("input");return A.type="file",!A.disabled}),Modernizr.addTest("formattribute",function(){var A,e=createElement("form"),t=createElement("input"),n=createElement("div"),r="formtest"+(new Date).getTime(),o=!1;e.id=r;try{t.setAttribute("form",r)}catch(i){document.createAttribute&&(A=document.createAttribute("form"),A.nodeValue=r,t.setAttributeNode(A))}return n.appendChild(e),n.appendChild(t),docElement.appendChild(n),o=e.elements&&1===e.elements.length&&t.form==e,n.parentNode.removeChild(n),o}),Modernizr.addTest("placeholder","placeholder"in createElement("input")&&"placeholder"in createElement("textarea")),Modernizr.addTest("sandbox","sandbox"in createElement("iframe")),Modernizr.addTest("inlinesvg",function(){var A=createElement("div");return A.innerHTML="","http://www.w3.org/2000/svg"==("undefined"!=typeof SVGRect&&A.firstChild&&A.firstChild.namespaceURI)}),Modernizr.addTest("textareamaxlength",!!("maxLength"in createElement("textarea"))),Modernizr.addTest("videocrossorigin","crossOrigin"in createElement("video")),Modernizr.addAsyncTest(function(){if(Modernizr.webglextensions=!1,Modernizr.webgl){var A,e,t;try{A=createElement("canvas"),e=A.getContext("webgl")||A.getContext("experimental-webgl"),t=e.getSupportedExtensions()}catch(n){return}e!==undefined&&(Modernizr.webglextensions=new Boolean(!0));for(var r=-1,o=t.length;++r7}),Modernizr.addTest("inputsearchevent",hasEvent("search")),Modernizr.addTest("ambientlight",hasEvent("devicelight",window));var inputElem=createElement("input"),inputattrs="autocomplete autofocus list placeholder max min multiple pattern required step".split(" "),attrs={};Modernizr.input=function(A){for(var e=0,t=A.length;t>e;e++)attrs[A[e]]=!!(A[e]in inputElem);return attrs.list&&(attrs.list=!(!createElement("datalist")||!window.HTMLDataListElement)),attrs}(inputattrs),Modernizr.addTest("datalistelem",Modernizr.input.list);var inputtypes="search tel url email datetime date month week time datetime-local number range color".split(" "),inputs={};Modernizr.inputtypes=function(A){for(var e,t,n,r=A.length,o="1)",i=0;r>i;i++)inputElem.setAttribute("type",e=A[i]),n="text"!==inputElem.type&&"style"in inputElem,n&&(inputElem.value=o,inputElem.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(e)&&inputElem.style.WebkitAppearance!==undefined?(docElement.appendChild(inputElem),t=document.defaultView,n=t.getComputedStyle&&"textfield"!==t.getComputedStyle(inputElem,null).WebkitAppearance&&0!==inputElem.offsetHeight,docElement.removeChild(inputElem)):/^(search|tel)$/.test(e)||(n=/^(url|email)$/.test(e)?inputElem.checkValidity&&inputElem.checkValidity()===!1:inputElem.value!=o)),inputs[A[i]]=!!n;return inputs}(inputtypes),Modernizr.addTest("videoloop","loop"in createElement("video"));var prefixes=ModernizrProto._config.usePrefixes?" -webkit- -moz- -o- -ms- ".split(" "):["",""];ModernizrProto._prefixes=prefixes,Modernizr.addTest("csscalc",function(){var A="width:",e="calc(10px);",t=createElement("a");return t.style.cssText=A+prefixes.join(e+A),!!t.style.length}),Modernizr.addTest("cubicbezierrange",function(){var A=createElement("a");return A.style.cssText=prefixes.join("transition-timing-function:cubic-bezier(1,0,0,1.1); "),!!A.style.length}),Modernizr.addTest("cssgradients",function(){for(var A,e="background-image:",t="gradient(linear,left top,right bottom,from(#9f9),to(white));",n="",r=0,o=prefixes.length-1;o>r;r++)A=0===r?"to ":"",n+=e+prefixes[r]+"linear-gradient("+A+"left top, #9f9, white);";Modernizr._config.usePrefixes&&(n+=e+"-webkit-"+t);var i=createElement("a"),d=i.style;return d.cssText=n,(""+d.backgroundImage).indexOf("gradient")>-1}),Modernizr.addTest("opacity",function(){var A=createElement("a").style;return A.cssText=prefixes.join("opacity:.55;"),/^0.55$/.test(A.opacity)}),Modernizr.addTest("csspositionsticky",function(){var A="position:",e="sticky",t=createElement("a"),n=t.style;return n.cssText=A+prefixes.join(e+";"+A).slice(0,-A.length),-1!==n.position.indexOf(e)});var modElem={elem:createElement("modernizr")};Modernizr._q.push(function(){delete modElem.elem}),Modernizr.addTest("csschunit",function(){var A,e=modElem.elem.style;try{e.fontSize="3ch",A=-1!==e.fontSize.indexOf("ch")}catch(t){A=!1}return A}),Modernizr.addTest("cssexunit",function(){var A,e=modElem.elem.style;try{e.fontSize="3ex",A=-1!==e.fontSize.indexOf("ex")}catch(t){A=!1}return A}),Modernizr.addTest("hsla",function(){var A=createElement("a").style;return A.cssText="background-color:hsla(120,40%,100%,.5)",contains(A.backgroundColor,"rgba")||contains(A.backgroundColor,"hsla")}),Modernizr.addTest("videopreload","preload"in createElement("video")),Modernizr.addTest("getUserMedia","mediaDevices"in navigator&&"getUserMedia"in navigator.mediaDevices),Modernizr.addTest("websocketsbinary",function(){var A,e="https:"==location.protocol?"wss":"ws";if("WebSocket"in window){if(A="binaryType"in WebSocket.prototype)return A;try{return!!new WebSocket(e+"://.").binaryType}catch(t){}}return!1}),Modernizr.addTest("atobbtoa","atob"in window&&"btoa"in window,{aliases:["atob-btoa"]}),Modernizr.addTest("sharedworkers","SharedWorker"in window),Modernizr.addTest("bdi",function(){var A=createElement("div"),e=createElement("bdi");e.innerHTML="إ",A.appendChild(e),docElement.appendChild(A);var t="rtl"===computedStyle(e,null,"direction");return docElement.removeChild(A),t});var testXhrType=function(A){if("undefined"==typeof XMLHttpRequest)return!1;var e=new XMLHttpRequest;e.open("get","/",!0);try{e.responseType=A}catch(t){return!1}return"response"in e&&e.responseType==A};Modernizr.addTest("xhrresponsetypearraybuffer",testXhrType("arraybuffer")),Modernizr.addTest("xhrresponsetypeblob",testXhrType("blob")),Modernizr.addTest("xhrresponsetypedocument",testXhrType("document")),Modernizr.addTest("xhrresponsetypejson",testXhrType("json")),Modernizr.addTest("xhrresponsetypetext",testXhrType("text"));var toStringFn={}.toString;Modernizr.addTest("svgclippaths",function(){return!!document.createElementNS&&/SVGClipPath/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","clipPath")))}),Modernizr.addTest("svgforeignobject",function(){return!!document.createElementNS&&/SVGForeignObject/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))}),Modernizr.addTest("smil",function(){return!!document.createElementNS&&/SVGAnimate/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","animate")))});var testStyles=ModernizrProto.testStyles=injectElementWithStyles;Modernizr.addTest("hiddenscroll",function(){return testStyles("#modernizr {width:100px;height:100px;overflow:scroll}",function(A){return A.offsetWidth===A.clientWidth})}),Modernizr.addTest("mathml",function(){var A;return testStyles("#modernizr{position:absolute;display:inline-block}",function(e){e.innerHTML+="xxyy",A=e.offsetHeight>e.offsetWidth}),A}),Modernizr.addTest("touchevents",function(){var A;if("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)A=!0;else{var e=["@media (",prefixes.join("touch-enabled),("),"heartz",")","{#modernizr{top:9px;position:absolute}}"].join("");testStyles(e,function(e){A=9===e.offsetTop})}return A}),Modernizr.addTest("unicoderange",function(){return Modernizr.testStyles('@font-face{font-family:"unicodeRange";src:local("Arial");unicode-range:U+0020,U+002E}#modernizr span{font-size:20px;display:inline-block;font-family:"unicodeRange",monospace}#modernizr .mono{font-family:monospace}',function(A){for(var e=[".",".","m","m"],t=0;t=9;return e||t}();blacklist?Modernizr.addTest("fontface",!1):testStyles('@font-face {font-family:"font";src:url("https://")}',function(A,e){var t=document.getElementById("smodernizr"),n=t.sheet||t.styleSheet,r=n?n.cssRules&&n.cssRules[0]?n.cssRules[0].cssText:n.cssText||"":"",o=/src/i.test(r)&&0===r.indexOf(e.split(" ")[0]);Modernizr.addTest("fontface",o); -}),testStyles('#modernizr{font:0/0 a}#modernizr:after{content:":)";visibility:hidden;font:7px/1 a}',function(A){Modernizr.addTest("generatedcontent",A.offsetHeight>=6)}),Modernizr.addTest("hairline",function(){return testStyles("#modernizr {border:.5px solid transparent}",function(A){return 1===A.offsetHeight})}),Modernizr.addTest("cssinvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:invalid{width:50px}",function(A){var e=createElement("input");return e.required=!0,A.appendChild(e),e.clientWidth>10})}),testStyles("#modernizr div {width:100px} #modernizr :last-child{width:200px;display:block}",function(A){Modernizr.addTest("lastchild",A.lastChild.offsetWidth>A.firstChild.offsetWidth)},2),testStyles("#modernizr div {width:1px} #modernizr div:nth-child(2n) {width:2px;}",function(A){for(var e=A.getElementsByTagName("div"),t=!0,n=0;5>n;n++)t=t&&e[n].offsetWidth===n%2+1;Modernizr.addTest("nthchild",t)},5),testStyles("#modernizr{overflow: scroll; width: 40px; height: 40px; }#"+prefixes.join("scrollbar{width:10px} #modernizr::").split("#").slice(1).join("#")+"scrollbar{width:10px}",function(A){Modernizr.addTest("cssscrollbar","scrollWidth"in A&&30==A.scrollWidth)}),Modernizr.addTest("siblinggeneral",function(){return testStyles("#modernizr div {width:100px} #modernizr div ~ div {width:200px;display:block}",function(A){return 200==A.lastChild.offsetWidth},2)}),testStyles("#modernizr{position: absolute; top: -10em; visibility:hidden; font: normal 10px arial;}#subpixel{float: left; font-size: 33.3333%;}",function(A){var e=A.firstChild;e.innerHTML="This is a text written in Arial",Modernizr.addTest("subpixelfont",window.getComputedStyle?"44px"!==window.getComputedStyle(e,null).getPropertyValue("width"):!1)},1,["subpixel"]),Modernizr.addTest("cssvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:valid{width:50px}",function(A){var e=createElement("input");return A.appendChild(e),e.clientWidth>10})}),testStyles("#modernizr { height: 50vh; }",function(A){var e=parseInt(window.innerHeight/2,10),t=parseInt(computedStyle(A,null,"height"),10);Modernizr.addTest("cssvhunit",roundedEquals(t,e))}),testStyles("#modernizr1{width: 50vmax}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(A){var e=A.childNodes[2],t=A.childNodes[1],n=A.childNodes[0],r=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=n.clientWidth/100,i=n.clientHeight/100,d=parseInt(50*Math.max(o,i),10),a=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvmaxunit",roundedEquals(d,a)||roundedEquals(d,a-r))},3),testStyles("#modernizr1{width: 50vm;width:50vmin}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(A){var e=A.childNodes[2],t=A.childNodes[1],n=A.childNodes[0],r=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=n.clientWidth/100,i=n.clientHeight/100,d=parseInt(50*Math.min(o,i),10),a=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvminunit",roundedEquals(d,a)||roundedEquals(d,a-r))},3),testStyles("#modernizr { width: 50vw; }",function(A){var e=parseInt(window.innerWidth/2,10),t=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvwunit",roundedEquals(t,e))}),Modernizr.addTest("details",function(){var A,e=createElement("details");return"open"in e?(testStyles("#modernizr details{display:block}",function(t){t.appendChild(e),e.innerHTML="ab",A=e.offsetHeight,e.open=!0,A=A!=e.offsetHeight}),A):!1}),Modernizr.addTest("oninput",function(){var A,e=createElement("input");if(e.setAttribute("oninput","return"),hasEvent("oninput",docElement)||"function"==typeof e.oninput)return!0;try{var t=document.createEvent("KeyboardEvent");A=!1;var n=function(e){A=!0,e.preventDefault(),e.stopPropagation()};t.initKeyEvent("keypress",!0,!0,window,!1,!1,!1,!1,0,"e".charCodeAt(0)),docElement.appendChild(e),e.addEventListener("input",n,!1),e.focus(),e.dispatchEvent(t),e.removeEventListener("input",n,!1),docElement.removeChild(e)}catch(r){A=!1}return A}),Modernizr.addTest("formvalidation",function(){var A=createElement("form");if(!("checkValidity"in A&&"addEventListener"in A))return!1;if("reportValidity"in A)return!0;var e,t=!1;return Modernizr.formvalidationapi=!0,A.addEventListener("submit",function(A){(!window.opera||window.operamini)&&A.preventDefault(),A.stopPropagation()},!1),A.innerHTML='',testStyles("#modernizr form{position:absolute;top:-99999em}",function(n){n.appendChild(A),e=A.getElementsByTagName("input")[0],e.addEventListener("invalid",function(A){t=!0,A.preventDefault(),A.stopPropagation()},!1),Modernizr.formvalidationmessage=!!e.validationMessage,A.getElementsByTagName("button")[0].click()}),t}),Modernizr.addTest("localizednumber",function(){if(!Modernizr.inputtypes.number)return!1;if(!Modernizr.formvalidation)return!1;var A,e=createElement("div"),t=getBody(),n=function(){return docElement.insertBefore(t,docElement.firstElementChild||docElement.firstChild)}();e.innerHTML='';var r=e.childNodes[0];n.appendChild(e),r.focus();try{document.execCommand("SelectAll",!1),document.execCommand("InsertText",!1,"1,1")}catch(o){}return A="number"===r.type&&1.1===r.valueAsNumber&&r.checkValidity(),n.removeChild(e),t.fake&&n.parentNode.removeChild(n),A});var mq=function(){var A=window.matchMedia||window.msMatchMedia;return A?function(e){var t=A(e);return t&&t.matches||!1}:function(A){var e=!1;return injectElementWithStyles("@media "+A+" { #modernizr { position: absolute; } }",function(A){e="absolute"==(window.getComputedStyle?window.getComputedStyle(A,null):A.currentStyle).position}),e}}();ModernizrProto.mq=mq,Modernizr.addTest("mediaqueries",mq("only all"));var hasOwnProp;!function(){var A={}.hasOwnProperty;hasOwnProp=is(A,"undefined")||is(A.call,"undefined")?function(A,e){return e in A&&is(A.constructor.prototype[e],"undefined")}:function(e,t){return A.call(e,t)}}(),ModernizrProto._l={},ModernizrProto.on=function(A,e){this._l[A]||(this._l[A]=[]),this._l[A].push(e),Modernizr.hasOwnProperty(A)&&setTimeout(function(){Modernizr._trigger(A,Modernizr[A])},0)},ModernizrProto._trigger=function(A,e){if(this._l[A]){var t=this._l[A];setTimeout(function(){var A,n;for(A=0;Ar?void(e=setTimeout(A,t)):(o.removeEventListener("playing",A,!1),addTest("videoautoplay",d),void(o.parentNode&&o.parentNode.removeChild(o)))}var e,t=200,n=5,r=0,o=createElement("video"),i=o.style;if(!(Modernizr.video&&"autoplay"in o))return void addTest("videoautoplay",!1);i.position="absolute",i.height=0,i.width=0;try{if(Modernizr.video.ogg)o.src="data:video/ogg;base64,T2dnUwACAAAAAAAAAABmnCATAAAAAHDEixYBKoB0aGVvcmEDAgEAAQABAAAQAAAQAAAAAAAFAAAAAQAAAAAAAAAAAGIAYE9nZ1MAAAAAAAAAAAAAZpwgEwEAAAACrA7TDlj///////////////+QgXRoZW9yYSsAAABYaXBoLk9yZyBsaWJ0aGVvcmEgMS4xIDIwMDkwODIyIChUaHVzbmVsZGEpAQAAABoAAABFTkNPREVSPWZmbXBlZzJ0aGVvcmEtMC4yOYJ0aGVvcmG+zSj3uc1rGLWpSUoQc5zmMYxSlKQhCDGMYhCEIQhAAAAAAAAAAAAAEW2uU2eSyPxWEvx4OVts5ir1aKtUKBMpJFoQ/nk5m41mUwl4slUpk4kkghkIfDwdjgajQYC8VioUCQRiIQh8PBwMhgLBQIg4FRba5TZ5LI/FYS/Hg5W2zmKvVoq1QoEykkWhD+eTmbjWZTCXiyVSmTiSSCGQh8PB2OBqNBgLxWKhQJBGIhCHw8HAyGAsFAiDgUCw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDAwPEhQUFQ0NDhESFRUUDg4PEhQVFRUOEBETFBUVFRARFBUVFRUVEhMUFRUVFRUUFRUVFRUVFRUVFRUVFRUVEAwLEBQZGxwNDQ4SFRwcGw4NEBQZHBwcDhATFhsdHRwRExkcHB4eHRQYGxwdHh4dGxwdHR4eHh4dHR0dHh4eHRALChAYKDM9DAwOExo6PDcODRAYKDlFOA4RFh0zV1A+EhYlOkRtZ00YIzdAUWhxXDFATldneXhlSFxfYnBkZ2MTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTEhIVGRoaGhoSFBYaGhoaGhUWGRoaGhoaGRoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhESFh8kJCQkEhQYIiQkJCQWGCEkJCQkJB8iJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCQREhgvY2NjYxIVGkJjY2NjGBo4Y2NjY2MvQmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRISEhUXGBkbEhIVFxgZGxwSFRcYGRscHRUXGBkbHB0dFxgZGxwdHR0YGRscHR0dHhkbHB0dHR4eGxwdHR0eHh4REREUFxocIBERFBcaHCAiERQXGhwgIiUUFxocICIlJRcaHCAiJSUlGhwgIiUlJSkcICIlJSUpKiAiJSUlKSoqEBAQFBgcICgQEBQYHCAoMBAUGBwgKDBAFBgcICgwQEAYHCAoMEBAQBwgKDBAQEBgICgwQEBAYIAoMEBAQGCAgAfF5cdH1e3Ow/L66wGmYnfIUbwdUTe3LMRbqON8B+5RJEvcGxkvrVUjTMrsXYhAnIwe0dTJfOYbWrDYyqUrz7dw/JO4hpmV2LsQQvkUeGq1BsZLx+cu5iV0e0eScJ91VIQYrmqfdVSK7GgjOU0oPaPOu5IcDK1mNvnD+K8LwS87f8Jx2mHtHnUkTGAurWZlNQa74ZLSFH9oF6FPGxzLsjQO5Qe0edcpttd7BXBSqMCL4k/4tFrHIPuEQ7m1/uIWkbDMWVoDdOSuRQ9286kvVUlQjzOE6VrNguN4oRXYGkgcnih7t13/9kxvLYKQezwLTrO44sVmMPgMqORo1E0sm1/9SludkcWHwfJwTSybR4LeAz6ugWVgRaY8mV/9SluQmtHrzsBtRF/wPY+X0JuYTs+ltgrXAmlk10xQHmTu9VSIAk1+vcvU4ml2oNzrNhEtQ3CysNP8UeR35wqpKUBdGdZMSjX4WVi8nJpdpHnbhzEIdx7mwf6W1FKAiucMXrWUWVjyRf23chNtR9mIzDoT/6ZLYailAjhFlZuvPtSeZ+2oREubDoWmT3TguY+JHPdRVSLKxfKH3vgNqJ/9emeEYikGXDFNzaLjvTeGAL61mogOoeG3y6oU4rW55ydoj0lUTSR/mmRhPmF86uwIfzp3FtiufQCmppaHDlGE0r2iTzXIw3zBq5hvaTldjG4CPb9wdxAme0SyedVKczJ9AtYbgPOzYKJvZZImsN7ecrxWZg5dR6ZLj/j4qpWsIA+vYwE+Tca9ounMIsrXMB4Stiib2SPQtZv+FVIpfEbzv8ncZoLBXc3YBqTG1HsskTTotZOYTG+oVUjLk6zhP8bg4RhMUNtfZdO7FdpBuXzhJ5Fh8IKlJG7wtD9ik8rWOJxy6iQ3NwzBpQ219mlyv+FLicYs2iJGSE0u2txzed++D61ZWCiHD/cZdQVCqkO2gJpdpNaObhnDfAPrT89RxdWFZ5hO3MseBSIlANppdZNIV/Rwe5eLTDvkfWKzFnH+QJ7m9QWV1KdwnuIwTNtZdJMoXBf74OhRnh2t+OTGL+AVUnIkyYY+QG7g9itHXyF3OIygG2s2kud679ZWKqSFa9n3IHD6MeLv1lZ0XyduRhiDRtrNnKoyiFVLcBm0ba5Yy3fQkDh4XsFE34isVpOzpa9nR8iCpS4HoxG2rJpnRhf3YboVa1PcRouh5LIJv/uQcPNd095ickTaiGBnWLKVWRc0OnYTSyex/n2FofEPnDG8y3PztHrzOLK1xo6RAml2k9owKajOC0Wr4D5x+3nA0UEhK2m198wuBHF3zlWWVKWLN1CHzLClUfuoYBcx4b1llpeBKmbayaR58njtE9onD66lUcsg0Spm2snsb+8HaJRn4dYcLbCuBuYwziB8/5U1C1DOOz2gZjSZtrLJk6vrLF3hwY4Io9xuT/ruUFRSBkNtUzTOWhjh26irLEPx4jPZL3Fo3QrReoGTTM21xYTT9oFdhTUIvjqTkfkvt0bzgVUjq/hOYY8j60IaO/0AzRBtqkTS6R5ellZd5uKdzzhb8BFlDdAcrwkE0rbXTOPB+7Y0FlZO96qFL4Ykg21StJs8qIW7h16H5hGiv8V2Cflau7QVDepTAHa6Lgt6feiEvJDM21StJsmOH/hynURrKxvUpQ8BH0JF7BiyG2qZpnL/7AOU66gt+reLEXY8pVOCQvSsBtqZTNM8bk9ohRcwD18o/WVkbvrceVKRb9I59IEKysjBeTMmmbA21xu/6iHadLRxuIzkLpi8wZYmmbbWi32RVAUjruxWlJ//iFxE38FI9hNKOoCdhwf5fDe4xZ81lgREhK2m1j78vW1CqkuMu/AjBNK210kzRUX/B+69cMMUG5bYrIeZxVSEZISmkzbXOi9yxwIfPgdsov7R71xuJ7rFcACjG/9PzApqFq7wEgzNJm2suWESPuwrQvejj7cbnQxMkxpm21lUYJL0fKmogPPqywn7e3FvB/FCNxPJ85iVUkCE9/tLKx31G4CgNtWTTPFhMvlu8G4/TrgaZttTChljfNJGgOT2X6EqpETy2tYd9cCBI4lIXJ1/3uVUllZEJz4baqGF64yxaZ+zPLYwde8Uqn1oKANtUrSaTOPHkhvuQP3bBlEJ/LFe4pqQOHUI8T8q7AXx3fLVBgSCVpMba55YxN3rv8U1Dv51bAPSOLlZWebkL8vSMGI21lJmmeVxPRwFlZF1CpqCN8uLwymaZyjbXHCRytogPN3o/n74CNykfT+qqRv5AQlHcRxYrC5KvGmbbUwmZY/29BvF6C1/93x4WVglXDLFpmbapmF89HKTogRwqqSlGbu+oiAkcWFbklC6Zhf+NtTLFpn8oWz+HsNRVSgIxZWON+yVyJlE5tq/+GWLTMutYX9ekTySEQPLVNQQ3OfycwJBM0zNtZcse7CvcKI0V/zh16Dr9OSA21MpmmcrHC+6pTAPHPwoit3LHHqs7jhFNRD6W8+EBGoSEoaZttTCZljfduH/fFisn+dRBGAZYtMzbVMwvul/T/crK1NQh8gN0SRRa9cOux6clC0/mDLFpmbarmF8/e6CopeOLCNW6S/IUUg3jJIYiAcDoMcGeRbOvuTPjXR/tyo79LK3kqqkbxkkMRAOB0GODPItnX3Jnxro/25Ud+llbyVVSN4ySGIgHA6DHBnkWzr7kz410f7cqO/Syt5KqpFVJwn6gBEvBM0zNtZcpGOEPiysW8vvRd2R0f7gtjhqUvXL+gWVwHm4XJDBiMpmmZtrLfPwd/IugP5+fKVSysH1EXreFAcEhelGmbbUmZY4Xdo1vQWVnK19P4RuEnbf0gQnR+lDCZlivNM22t1ESmopPIgfT0duOfQrsjgG4tPxli0zJmF5trdL1JDUIUT1ZXSqQDeR4B8mX3TrRro/2McGeUvLtwo6jIEKMkCUXWsLyZROd9P/rFYNtXPBli0z398iVUlVKAjFlY437JXImUTm2r/4ZYtMy61hf16RPJIU9nZ1MABAwAAAAAAAAAZpwgEwIAAABhp658BScAAAAAAADnUFBQXIDGXLhwtttNHDhw5OcpQRMETBEwRPduylKVB0HRdF0A";else{ -if(!Modernizr.video.h264)return void addTest("videoautoplay",!1);o.src="data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAs1tZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0OCByMjYwMSBhMGNkN2QzIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTEgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTEwIHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAD2WIhAA3//728P4FNjuZQQAAAu5tb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPoAAAAZAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAACGHRyYWsAAABcdGtoZAAAAAMAAAAAAAAAAAAAAAEAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAgAAAAIAAAAAACRlZHRzAAAAHGVsc3QAAAAAAAAAAQAAAGQAAAAAAAEAAAAAAZBtZGlhAAAAIG1kaGQAAAAAAAAAAAAAAAAAACgAAAAEAFXEAAAAAAAtaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAFZpZGVvSGFuZGxlcgAAAAE7bWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAAA+3N0YmwAAACXc3RzZAAAAAAAAAABAAAAh2F2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAgACAEgAAABIAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAxYXZjQwFkAAr/4QAYZ2QACqzZX4iIhAAAAwAEAAADAFA8SJZYAQAGaOvjyyLAAAAAGHN0dHMAAAAAAAAAAQAAAAEAAAQAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAAsUAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjU2LjQwLjEwMQ=="}}catch(d){return void addTest("videoautoplay",!1)}o.setAttribute("autoplay",""),i.cssText="display:none",docElement.appendChild(o),setTimeout(function(){o.addEventListener("playing",A,!1),e=setTimeout(A,t)},0)});var omPrefixes="Moz O ms Webkit",domPrefixes=ModernizrProto._config.usePrefixes?omPrefixes.toLowerCase().split(" "):[];ModernizrProto._domPrefixes=domPrefixes,Modernizr.addTest("pointerevents",function(){var A=!1,e=domPrefixes.length;for(A=Modernizr.hasEvent("pointerdown");e--&&!A;)hasEvent(domPrefixes[e]+"pointerdown")&&(A=!0);return A}),Modernizr.addTest("fileinputdirectory",function(){var A=createElement("input"),e="directory";if(A.type="file",e in A)return!0;for(var t=0,n=domPrefixes.length;n>t;t++)if(domPrefixes[t]+e in A)return!0;return!1});var cssomPrefixes=ModernizrProto._config.usePrefixes?omPrefixes.split(" "):[];ModernizrProto._cssomPrefixes=cssomPrefixes;var atRule=function(A){var e,t=prefixes.length,n=window.CSSRule;if("undefined"==typeof n)return undefined;if(!A)return!1;if(A=A.replace(/^@/,""),e=A.replace(/-/g,"_").toUpperCase()+"_RULE",e in n)return"@"+A;for(var r=0;t>r;r++){var o=prefixes[r],i=o.toUpperCase()+"_"+e;if(i in n)return"@-"+o.toLowerCase()+"-"+A}return!1};ModernizrProto.atRule=atRule;var mStyle={style:modElem.elem.style};Modernizr._q.unshift(function(){delete mStyle.style});var testProp=ModernizrProto.testProp=function(A,e,t){return testProps([A],undefined,e,t)};Modernizr.addTest("textshadow",testProp("textShadow","1px 1px")),ModernizrProto.testAllProps=testPropsAll;var prefixed=ModernizrProto.prefixed=function(A,e,t){return 0===A.indexOf("@")?atRule(A):(-1!=A.indexOf("-")&&(A=cssToDOM(A)),e?testPropsAll(A,e,t):testPropsAll(A,"pfx"))};Modernizr.addAsyncTest(function(){var A;try{A=prefixed("indexedDB",window)}catch(e){}if(A){var t="modernizr-"+Math.random(),n=A.open(t);n.onerror=function(){n.error&&"InvalidStateError"===n.error.name?addTest("indexeddb",!1):(addTest("indexeddb",!0),detectDeleteDatabase(A,t))},n.onsuccess=function(){addTest("indexeddb",!0),detectDeleteDatabase(A,t)}}else addTest("indexeddb",!1)}),Modernizr.addAsyncTest(function(){var A,e,t,n,r="detect-blob-support",o=!1;try{A=prefixed("indexedDB",window)}catch(i){}if(!Modernizr.indexeddb||!Modernizr.indexeddb.deletedatabase)return!1;try{A.deleteDatabase(r).onsuccess=function(){e=A.open(r,1),e.onupgradeneeded=function(){e.result.createObjectStore("store")},e.onsuccess=function(){t=e.result;try{n=t.transaction("store","readwrite").objectStore("store").put(new Blob,"key"),n.onsuccess=function(){o=!0},n.onerror=function(){o=!1}}catch(i){o=!1}finally{addTest("indexeddbblob",o),t.close(),A.deleteDatabase(r)}}}}catch(i){addTest("indexeddbblob",!1)}}),Modernizr.addTest("batteryapi",!!prefixed("battery",navigator),{aliases:["battery-api"]});var crypto=prefixed("crypto",window);Modernizr.addTest("crypto",!!prefixed("subtle",crypto)),Modernizr.addTest("dart",!!prefixed("startDart",navigator)),Modernizr.addTest("forcetouch",function(){return hasEvent(prefixed("mouseforcewillbegin",window,!1),window)?MouseEvent.WEBKIT_FORCE_AT_MOUSE_DOWN&&MouseEvent.WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN:!1}),Modernizr.addTest("fullscreen",!(!prefixed("exitFullscreen",document,!1)&&!prefixed("cancelFullScreen",document,!1))),Modernizr.addTest("gamepads",!!prefixed("getGamepads",navigator)),Modernizr.addTest("intl",!!prefixed("Intl",window)),Modernizr.addTest("pagevisibility",!!prefixed("hidden",document,!1)),Modernizr.addTest("performance",!!prefixed("performance",window)),Modernizr.addTest("pointerlock",!!prefixed("exitPointerLock",document)),Modernizr.addTest("quotamanagement",function(){var A=prefixed("temporaryStorage",navigator),e=prefixed("persistentStorage",navigator);return!(!A||!e)}),Modernizr.addTest("requestanimationframe",!!prefixed("requestAnimationFrame",window),{aliases:["raf"]}),Modernizr.addTest("vibrate",!!prefixed("vibrate",navigator)),Modernizr.addTest("webintents",!!prefixed("startActivity",navigator)),Modernizr.addTest("lowbattery",function(){var A=.2,e=prefixed("battery",navigator);return!!(e&&!e.charging&&e.level<=A)});var crypto=prefixed("crypto",window),supportsGetRandomValues;if(crypto&&"getRandomValues"in crypto&&"Uint32Array"in window){var array=new Uint32Array(10),values=crypto.getRandomValues(array);supportsGetRandomValues=values&&is(values[0],"number")}Modernizr.addTest("getrandomvalues",!!supportsGetRandomValues),Modernizr.addTest("backgroundblendmode",prefixed("backgroundBlendMode","text")),Modernizr.addTest("objectfit",!!prefixed("objectFit"),{aliases:["object-fit"]}),Modernizr.addTest("regions",function(){if(isSVG)return!1;var A=prefixed("flowFrom"),e=prefixed("flowInto"),t=!1;if(!A||!e)return t;var n=createElement("iframe"),r=createElement("div"),o=createElement("div"),i=createElement("div"),d="modernizr_flow_for_regions_check";o.innerText="M",r.style.cssText="top: 150px; left: 150px; padding: 0px;",i.style.cssText="width: 50px; height: 50px; padding: 42px;",i.style[A]=d,r.appendChild(o),r.appendChild(i),docElement.appendChild(r);var a,s,l=o.getBoundingClientRect();return o.style[e]=d,a=o.getBoundingClientRect(),s=parseInt(a.left-l.left,10),docElement.removeChild(r),42==s?t=!0:(docElement.appendChild(n),l=n.getBoundingClientRect(),n.style[e]=d,a=n.getBoundingClientRect(),l.height>0&&l.height!==a.height&&0===a.height&&(t=!0)),o=i=r=n=undefined,t}),Modernizr.addTest("wrapflow",function(){var A=prefixed("wrapFlow");if(!A||isSVG)return!1;var e=A.replace(/([A-Z])/g,function(A,e){return"-"+e.toLowerCase()}).replace(/^ms-/,"-ms-"),t=createElement("div"),n=createElement("div"),r=createElement("span");n.style.cssText="position: absolute; left: 50px; width: 100px; height: 20px;"+e+":end;",r.innerText="X",t.appendChild(n),t.appendChild(r),docElement.appendChild(t);var o=r.offsetLeft;return docElement.removeChild(t),n=r=t=undefined,150==o}),Modernizr.addTest("speechrecognition",!!prefixed("SpeechRecognition",window)),Modernizr.addTest("filesystem",!!prefixed("requestFileSystem",window)),Modernizr.addTest("requestautocomplete",!!prefixed("requestAutocomplete",createElement("form")));var url=prefixed("URL",window,!1);url=url&&window[url],Modernizr.addTest("bloburls",url&&"revokeObjectURL"in url&&"createObjectURL"in url),Modernizr.addAsyncTest(function(){function A(){addTest("transferables",!1),e()}function e(){d&&URL.revokeObjectURL(d),a&&a.terminate(),r&&clearTimeout(r)}var t=!!(Modernizr.blobconstructor&&Modernizr.bloburls&&Modernizr.webworkers&&Modernizr.typedarrays);if(!t)return addTest("transferables",!1);try{var n,r,o='var hello = "world"',i=new Blob([o],{type:"text/javascript"}),d=URL.createObjectURL(i),a=new Worker(d);a.onerror=A,r=setTimeout(A,200),n=new ArrayBuffer(1),a.postMessage(n,[n]),addTest("transferables",0===n.byteLength),e()}catch(s){A()}}),Modernizr.addTest("peerconnection",!!prefixed("RTCPeerConnection",window)),Modernizr.addTest("datachannel",function(){if(!Modernizr.peerconnection)return!1;for(var A=0,e=domPrefixes.length;e>A;A++){var t=window[domPrefixes[A]+"RTCPeerConnection"];if(t){var n=new t(null);return"createDataChannel"in n}}return!1}),Modernizr.addTest("matchmedia",!!prefixed("matchMedia",window)),ModernizrProto.testAllProps=testAllProps,Modernizr.addTest("ligatures",testAllProps("fontFeatureSettings",'"liga" 1')),Modernizr.addTest("cssanimations",testAllProps("animationName","a",!0)),Modernizr.addTest("csspseudoanimations",function(){var A=!1;if(!Modernizr.cssanimations||!window.getComputedStyle)return A;var e=["@",Modernizr._prefixes.join("keyframes csspseudoanimations { from { font-size: 10px; } }@").replace(/\@$/,""),'#modernizr:before { content:" "; font-size:5px;',Modernizr._prefixes.join("animation:csspseudoanimations 1ms infinite;"),"}"].join("");return Modernizr.testStyles(e,function(e){A="10px"===window.getComputedStyle(e,":before").getPropertyValue("font-size")}),A}),Modernizr.addTest("appearance",testAllProps("appearance")),Modernizr.addTest("backdropfilter",testAllProps("backdropFilter")),Modernizr.addTest("backgroundcliptext",function(){return testAllProps("backgroundClip","text")}),Modernizr.addTest("bgpositionxy",function(){return testAllProps("backgroundPositionX","3px",!0)&&testAllProps("backgroundPositionY","5px",!0)}),Modernizr.addTest("bgrepeatround",testAllProps("backgroundRepeat","round")),Modernizr.addTest("bgrepeatspace",testAllProps("backgroundRepeat","space")),Modernizr.addTest("backgroundsize",testAllProps("backgroundSize","100%",!0)),Modernizr.addTest("bgsizecover",testAllProps("backgroundSize","cover")),Modernizr.addTest("borderimage",testAllProps("borderImage","url() 1",!0)),Modernizr.addTest("borderradius",testAllProps("borderRadius","0px",!0)),Modernizr.addTest("boxshadow",testAllProps("boxShadow","1px 1px",!0)),Modernizr.addTest("boxsizing",testAllProps("boxSizing","border-box",!0)&&(document.documentMode===undefined||document.documentMode>7)),function(){Modernizr.addTest("csscolumns",function(){var A=!1,e=testAllProps("columnCount");try{A=!!e,A&&(A=new Boolean(A))}catch(t){}return A});for(var A,e,t=["Width","Span","Fill","Gap","Rule","RuleColor","RuleStyle","RuleWidth","BreakBefore","BreakAfter","BreakInside"],n=0;n9)}),Modernizr.addTest("flexbox",testAllProps("flexBasis","1px",!0)),Modernizr.addTest("flexboxlegacy",testAllProps("boxDirection","reverse",!0)),Modernizr.addTest("flexboxtweener",testAllProps("flexAlign","end",!0)),Modernizr.addTest("flexwrap",testAllProps("flexWrap","wrap",!0)),Modernizr.addAsyncTest(function(){function A(){function t(){try{var A=createElement("div"),e=createElement("span"),t=A.style,n=0,r=0,o=!1,i=document.body.firstElementChild||document.body.firstChild;return A.appendChild(e),e.innerHTML="Bacon ipsum dolor sit amet jerky velit in culpa hamburger et. Laborum dolor proident, enim dolore duis commodo et strip steak. Salami anim et, veniam consectetur dolore qui tenderloin jowl velit sirloin. Et ad culpa, fatback cillum jowl ball tip ham hock nulla short ribs pariatur aute. Pig pancetta ham bresaola, ut boudin nostrud commodo flank esse cow tongue culpa. Pork belly bresaola enim pig, ea consectetur nisi. Fugiat officia turkey, ea cow jowl pariatur ullamco proident do laborum velit sausage. Magna biltong sint tri-tip commodo sed bacon, esse proident aliquip. Ullamco ham sint fugiat, velit in enim sed mollit nulla cow ut adipisicing nostrud consectetur. Proident dolore beef ribs, laborum nostrud meatball ea laboris rump cupidatat labore culpa. Shankle minim beef, velit sint cupidatat fugiat tenderloin pig et ball tip. Ut cow fatback salami, bacon ball tip et in shank strip steak bresaola. In ut pork belly sed mollit tri-tip magna culpa veniam, short ribs qui in andouille ham consequat. Dolore bacon t-bone, velit short ribs enim strip steak nulla. Voluptate labore ut, biltong swine irure jerky. Cupidatat excepteur aliquip salami dolore. Ball tip strip steak in pork dolor. Ad in esse biltong. Dolore tenderloin exercitation ad pork loin t-bone, dolore in chicken ball tip qui pig. Ut culpa tongue, sint ribeye dolore ex shank voluptate hamburger. Jowl et tempor, boudin pork chop labore ham hock drumstick consectetur tri-tip elit swine meatball chicken ground round. Proident shankle mollit dolore. Shoulder ut duis t-bone quis reprehenderit. Meatloaf dolore minim strip steak, laboris ea aute bacon beef ribs elit shank in veniam drumstick qui. Ex laboris meatball cow tongue pork belly. Ea ball tip reprehenderit pig, sed fatback boudin dolore flank aliquip laboris eu quis. Beef ribs duis beef, cow corned beef adipisicing commodo nisi deserunt exercitation. Cillum dolor t-bone spare ribs, ham hock est sirloin. Brisket irure meatloaf in, boudin pork belly sirloin ball tip. Sirloin sint irure nisi nostrud aliqua. Nostrud nulla aute, enim officia culpa ham hock. Aliqua reprehenderit dolore sunt nostrud sausage, ea boudin pork loin ut t-bone ham tempor. Tri-tip et pancetta drumstick laborum. Ham hock magna do nostrud in proident. Ex ground round fatback, venison non ribeye in.",document.body.insertBefore(A,i),t.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justification:newspaper;",n=e.offsetHeight,r=e.offsetWidth,t.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justification:newspaper;"+prefixes.join("hyphens:auto; "),o=e.offsetHeight!=n||e.offsetWidth!=r,document.body.removeChild(A),A.removeChild(e),o}catch(d){return!1}}function n(A,e){try{var t=createElement("div"),n=createElement("span"),r=t.style,o=0,i=!1,d=!1,a=!1,s=document.body.firstElementChild||document.body.firstChild;return r.cssText="position:absolute;top:0;left:0;overflow:visible;width:1.25em;",t.appendChild(n),document.body.insertBefore(t,s),n.innerHTML="mm",o=n.offsetHeight,n.innerHTML="m"+A+"m",d=n.offsetHeight>o,e?(n.innerHTML="m
m",o=n.offsetWidth,n.innerHTML="m"+A+"m",a=n.offsetWidth>o):a=!0,d===!0&&a===!0&&(i=!0),document.body.removeChild(t),t.removeChild(n),i}catch(l){return!1}}function r(A){try{var e,t=createElement("input"),n=createElement("div"),r="lebowski",o=!1,i=document.body.firstElementChild||document.body.firstChild;n.innerHTML=r+A+r,document.body.insertBefore(n,i),document.body.insertBefore(t,n),t.setSelectionRange?(t.focus(),t.setSelectionRange(0,0)):t.createTextRange&&(e=t.createTextRange(),e.collapse(!0),e.moveEnd("character",0),e.moveStart("character",0),e.select());try{window.find?o=window.find(r+r):(e=window.self.document.body.createTextRange(),o=e.findText(r+r))}catch(d){o=!1}return document.body.removeChild(n),document.body.removeChild(t),o}catch(d){return!1}}return document.body||document.getElementsByTagName("body")[0]?(addTest("csshyphens",function(){if(!testAllProps("hyphens","auto",!0))return!1;try{return t()}catch(A){return!1}}),addTest("softhyphens",function(){try{return n("­",!0)&&n("​",!1)}catch(A){return!1}}),void addTest("softhyphensfind",function(){try{return r("­")&&r("​")}catch(A){return!1}})):void setTimeout(A,e)}var e=300;setTimeout(A,e)}),Modernizr.addTest("cssmask",testAllProps("maskRepeat","repeat-x",!0)),Modernizr.addTest("overflowscrolling",testAllProps("overflowScrolling","touch",!0)),Modernizr.addTest("cssreflections",testAllProps("boxReflect","above",!0)),Modernizr.addTest("cssresize",testAllProps("resize","both",!0)),Modernizr.addTest("scrollsnappoints",testAllProps("scrollSnapType")),Modernizr.addTest("shapes",testAllProps("shapeOutside","content-box",!0)),Modernizr.addTest("textalignlast",testAllProps("textAlignLast")),Modernizr.addTest("csstransforms",function(){return-1===navigator.userAgent.indexOf("Android 2.")&&testAllProps("transform","scale(1)",!0)}),Modernizr.addTest("csstransforms3d",function(){return!!testAllProps("perspective","1px",!0)}),Modernizr.addTest("csstransformslevel2",function(){return testAllProps("translate","45px",!0)}),Modernizr.addTest("csstransitions",testAllProps("transition","all",!0)),Modernizr.addTest("csspseudotransitions",function(){var A=!1;if(!Modernizr.csstransitions||!window.getComputedStyle)return A;var e='#modernizr:before { content:" "; font-size:5px;'+Modernizr._prefixes.join("transition:0s 100s;")+"}#modernizr.trigger:before { font-size:10px; }";return Modernizr.testStyles(e,function(e){window.getComputedStyle(e,":before").getPropertyValue("font-size"),e.className+="trigger",A="5px"===window.getComputedStyle(e,":before").getPropertyValue("font-size")}),A}),Modernizr.addTest("userselect",testAllProps("userSelect","none",!0)),Modernizr.addTest("variablefonts",testAllProps("fontVariationSettings")),testRunner(),setClasses(classes),delete ModernizrProto.addTest,delete ModernizrProto.addAsyncTest;for(var i=0;i + + diff --git a/tests/assets/modernizr/mobile-safari-14-1.json b/tests/assets/modernizr/mobile-safari-18.json similarity index 67% rename from tests/assets/modernizr/mobile-safari-14-1.json rename to tests/assets/modernizr/mobile-safari-18.json index 4e959333fa..e86fc3de20 100644 --- a/tests/assets/modernizr/mobile-safari-14-1.json +++ b/tests/assets/modernizr/mobile-safari-18.json @@ -11,6 +11,249 @@ "required": true, "step": true }, + "adownload": true, + "aping": true, + "areaping": true, + "ambientlight": false, + "applicationcache": false, + "audio": { + "ogg": "", + "mp3": "probably", + "opus": "probably", + "wav": "probably", + "m4a": "maybe" + }, + "audioloop": true, + "webaudio": true, + "batteryapi": false, + "battery-api": false, + "lowbattery": false, + "blobconstructor": true, + "blob-constructor": true, + "broadcastchannel": true, + "canvas": true, + "canvasblending": true, + "todataurljpeg": true, + "todataurlpng": true, + "todataurlwebp": false, + "canvaswinding": true, + "canvastext": true, + "clipboard": { + "read": true, + "readtext": true, + "write": true, + "writetext": true + }, + "contenteditable": true, + "contextmenu": false, + "cors": true, + "crypto": true, + "getrandomvalues": true, + "cssall": true, + "cssanimations": true, + "appearance": true, + "aspectratio": true, + "backdropfilter": true, + "backgroundblendmode": true, + "backgroundcliptext": true, + "bgpositionshorthand": true, + "bgpositionxy": true, + "bgrepeatround": true, + "bgrepeatspace": true, + "backgroundsize": true, + "bgsizecover": true, + "borderimage": true, + "borderradius": true, + "boxdecorationbreak": true, + "boxshadow": true, + "boxsizing": true, + "csscalc": true, + "checked": true, + "csschunit": true, + "csscolumns": { + "width": true, + "span": true, + "fill": true, + "gap": true, + "rule": true, + "rulecolor": true, + "rulestyle": true, + "rulewidth": true, + "breakbefore": true, + "breakafter": true, + "breakinside": true + }, + "cssgridlegacy": false, + "cssgrid": true, + "cubicbezierrange": true, + "customproperties": true, + "displayrunin": false, + "display-runin": false, + "displaytable": true, + "display-table": true, + "ellipsis": true, + "cssescape": true, + "cssexunit": true, + "supports": true, + "cssfilters": true, + "flexbox": true, + "flexboxlegacy": true, + "flexboxtweener": false, + "flexgap": true, + "flexwrap": true, + "focusvisible": true, + "focuswithin": true, + "fontdisplay": true, + "fontface": true, + "generatedcontent": true, + "cssgradients": true, + "hairline": true, + "hsla": true, + "cssinvalid": true, + "lastchild": true, + "cssmask": true, + "mediaqueries": true, + "multiplebgs": true, + "nthchild": true, + "objectfit": true, + "object-fit": true, + "opacity": true, + "overflowscrolling": true, + "csspointerevents": true, + "csspositionsticky": true, + "csspseudoanimations": true, + "csstransitions": true, + "csspseudotransitions": true, + "cssreflections": true, + "regions": false, + "cssremunit": true, + "cssresize": true, + "rgba": true, + "cssscrollbar": false, + "scrollsnappoints": true, + "shapes": true, + "siblinggeneral": true, + "subpixelfont": true, + "target": true, + "textalignlast": true, + "textdecoration": { + "line": true, + "style": true, + "color": true, + "skip": true, + "skipink": true + }, + "textshadow": true, + "csstransforms": true, + "csstransforms3d": true, + "csstransformslevel2": true, + "preserve3d": true, + "userselect": true, + "cssvalid": true, + "variablefonts": true, + "cssvhunit": true, + "cssvmaxunit": false, + "cssvminunit": true, + "cssvwunit": true, + "willchange": true, + "wrapflow": false, + "customelements": true, + "customprotocolhandler": false, + "dart": false, + "dataview": true, + "classlist": true, + "createelementattrs": false, + "createelement-attrs": false, + "dataset": true, + "documentfragment": true, + "hidden": true, + "intersectionobserver": true, + "microdata": false, + "mutationobserver": true, + "passiveeventlisteners": true, + "shadowroot": true, + "shadowrootlegacy": false, + "bdi": true, + "details": true, + "outputelem": true, + "picture": true, + "progressbar": true, + "meter": true, + "ruby": true, + "template": true, + "time": false, + "texttrackapi": true, + "track": true, + "unknownelements": true, + "emoji": true, + "es5array": true, + "es5date": true, + "es5function": true, + "es5object": true, + "strictmode": true, + "es5string": true, + "json": true, + "es5syntax": true, + "es5undefined": true, + "es5": true, + "es6array": true, + "arrow": true, + "es6class": true, + "es6collections": true, + "generators": true, + "es6math": true, + "es6number": true, + "es6object": true, + "promises": true, + "restparameters": true, + "spreadarray": true, + "stringtemplate": true, + "es6string": true, + "es6symbol": true, + "es7array": true, + "restdestructuringarray": true, + "restdestructuringobject": true, + "spreadobject": true, + "es8object": true, + "customevent": true, + "devicemotion": true, + "deviceorientation": true, + "eventlistener": true, + "forcetouch": false, + "hashchange": true, + "oninput": true, + "pointerevents": true, + "proximity": false, + "filereader": true, + "filesystem": false, + "flash": false, + "fullscreen": false, + "gamepads": true, + "geolocation": true, + "hiddenscroll": true, + "history": true, + "htmlimports": false, + "ie8compat": false, + "sandbox": true, + "seamless": false, + "srcdoc": true, + "imgcrossorigin": true, + "lazyloading": true, + "sizes": true, + "srcset": true, + "capture": true, + "fileinput": true, + "fileinputdirectory": true, + "inputformaction": true, + "input-formaction": true, + "formattribute": true, + "inputformenctype": true, + "input-formenctype": true, + "inputformmethod": true, + "inputformnovalidate": true, + "input-formnovalidate": true, + "inputformtarget": true, + "input-formtarget": true, "inputtypes": { "search": true, "tel": true, @@ -26,278 +269,140 @@ "range": true, "color": true }, - "htmlimports": false, - "history": true, - "ie8compat": false, - "applicationcache": false, - "blobconstructor": true, - "blob-constructor": true, - "cookies": true, - "cors": true, - "customelements": true, - "customprotocolhandler": false, - "customevent": true, - "dataview": true, - "eventlistener": true, - "geolocation": true, - "json": true, + "formvalidation": true, + "localizednumber": false, + "inputsearchevent": false, + "placeholder": true, + "requestautocomplete": false, + "intl": true, + "ligatures": true, + "olreversed": true, + "mathml": true, + "mediasource": false, + "hovermq": false, + "pointermq": true, "messagechannel": true, - "notification": false, - "postmessage": true, - "queryselector": true, - "serviceworker": true, - "svg": true, - "templatestrings": true, - "typedarrays": true, - "websockets": true, - "xdomainrequest": false, - "webaudio": true, - "cssescape": true, - "focuswithin": true, - "supports": true, - "target": true, - "microdata": false, - "mutationobserver": true, - "passiveeventlisteners": true, - "picture": true, - "es5array": true, - "es5date": true, - "es5function": true, "beacon": true, + "effectivetype": false, "lowbandwidth": false, "eventsource": true, "fetch": true, - "xhrresponsetype": true, - "xhr2": true, - "speechsynthesis": true, - "localstorage": true, - "sessionstorage": true, - "websqldatabase": true, - "es5object": true, - "svgfilters": true, - "strictmode": true, - "es5string": true, - "es5syntax": true, - "es5undefined": true, - "es5": true, - "es6array": true, - "arrow": true, - "es6collections": true, - "generators": true, - "es6math": true, - "es6number": true, - "es6object": true, - "promises": true, - "es6string": true, - "devicemotion": true, - "devicemotion2": true, - "deviceorientation": true, - "deviceorientation2": true, - "deviceorientation3": true, - "filereader": true, - "urlparser": true, - "urlsearchparams": true, - "framed": false, - "webworkers": true, - "contextmenu": false, - "cssall": true, - "willchange": true, - "classlist": true, - "documentfragment": true, - "contains": false, - "audio": true, - "canvas": true, - "canvastext": true, - "contenteditable": true, - "emoji": false, - "olreversed": true, - "userdata": false, - "video": true, - "vml": false, - "webanimations": true, - "webgl": true, - "adownload": true, - "audioloop": true, - "canvasblending": true, - "todataurljpeg": true, - "todataurlpng": true, - "todataurlwebp": false, - "canvaswinding": true, - "bgpositionshorthand": true, - "multiplebgs": true, - "csspointerevents": true, - "cssremunit": true, - "rgba": true, - "preserve3d": true, - "createelementattrs": false, - "createelement-attrs": false, - "dataset": true, - "hidden": true, - "outputelem": true, - "progressbar": true, - "meter": true, - "ruby": true, - "template": true, - "srcset": true, - "time": false, - "texttrackapi": true, - "track": true, - "unknownelements": true, - "inputformaction": true, - "input-formaction": true, - "inputformenctype": true, - "input-formenctype": true, - "inputformmethod": true, - "inputformtarget": false, - "input-formtarget": false, - "scriptasync": true, - "scriptdefer": true, - "stylescoped": false, - "capture": true, - "fileinput": true, - "formattribute": true, - "placeholder": true, - "sandbox": true, - "inlinesvg": true, - "textareamaxlength": true, - "videocrossorigin": true, - "webglextensions": true, - "seamless": false, - "srcdoc": true, - "imgcrossorigin": true, - "hashchange": true, - "inputsearchevent": false, - "ambientlight": false, - "datalistelem": true, - "videoloop": true, - "csscalc": true, - "cubicbezierrange": true, - "cssgradients": true, - "opacity": true, - "csspositionsticky": true, - "csschunit": true, - "cssexunit": true, - "hsla": true, - "videopreload": true, - "getusermedia": true, - "websocketsbinary": true, - "atobbtoa": true, - "atob-btoa": true, - "sharedworkers": true, - "bdi": true, "xhrresponsetypearraybuffer": true, "xhrresponsetypeblob": true, "xhrresponsetypedocument": true, "xhrresponsetypejson": true, "xhrresponsetypetext": true, - "svgclippaths": true, - "svgforeignobject": true, - "smil": true, - "hiddenscroll": true, - "mathml": true, - "touchevents": true, - "unicoderange": true, - "unicode": true, - "checked": true, - "displaytable": true, - "display-table": true, - "fontface": true, - "generatedcontent": true, - "hairline": true, - "cssinvalid": true, - "lastchild": true, - "nthchild": true, - "cssscrollbar": false, - "siblinggeneral": true, - "subpixelfont": true, - "cssvalid": true, - "cssvhunit": false, - "cssvmaxunit": false, - "cssvminunit": true, - "cssvwunit": true, - "details": true, - "oninput": true, - "formvalidation": true, - "localizednumber": false, - "mediaqueries": true, - "flash": false, - "proximity": false, - "sizes": true, - "hovermq": false, - "pointermq": true, - "svgasimg": true, - "pointerevents": true, - "fileinputdirectory": true, - "textshadow": true, - "batteryapi": false, - "battery-api": false, - "crypto": true, - "dart": false, - "forcetouch": false, - "fullscreen": false, - "gamepads": true, - "intl": true, + "xhrresponsetype": true, + "xhr2": true, + "notification": false, "pagevisibility": true, "performance": true, "pointerlock": false, - "quotamanagement": false, + "postmessage": { + "structuredclones": true + }, + "proxy": true, + "queryselector": true, + "prefetch": false, "requestanimationframe": true, "raf": true, - "vibrate": false, - "webintents": false, - "lowbattery": false, - "getrandomvalues": true, - "backgroundblendmode": true, - "objectfit": true, - "object-fit": true, - "regions": false, - "wrapflow": false, + "scriptasync": true, + "scriptdefer": true, + "scrolltooptions": true, + "serviceworker": true, "speechrecognition": true, - "filesystem": false, - "requestautocomplete": false, + "speechsynthesis": true, + "cookies": true, + "localstorage": true, + "quotamanagement": false, + "sessionstorage": true, + "userdata": false, + "websqldatabase": true, + "stylescoped": false, + "svg": true, + "svgasimg": true, + "svgclippaths": true, + "svgfilters": true, + "svgforeignobject": true, + "inlinesvg": true, + "smil": true, + "textareamaxlength": true, + "textencoder": true, + "textdecoder": true, + "typedarrays": true, + "unicoderange": true, "bloburls": true, - "transferables": true, + "urlparser": true, + "urlsearchparams": true, + "vibrate": false, + "video": { + "ogg": "", + "h264": "probably", + "h265": "", + "webm": "probably", + "vp9": "probably", + "hls": "probably", + "av1": "" + }, + "videocrossorigin": true, + "videoloop": true, + "videopreload": true, + "vml": false, + "webintents": false, + "webanimations": true, + "publickeycredential": true, + "webgl": true, + "webglextensions": { + "ANGLE_instanced_arrays": true, + "EXT_blend_minmax": true, + "EXT_clip_control": true, + "EXT_color_buffer_half_float": true, + "EXT_depth_clamp": true, + "EXT_frag_depth": true, + "EXT_polygon_offset_clamp": true, + "EXT_shader_texture_lod": true, + "EXT_texture_filter_anisotropic": true, + "EXT_sRGB": true, + "KHR_parallel_shader_compile": true, + "OES_element_index_uint": true, + "OES_fbo_render_mipmap": true, + "OES_standard_derivatives": true, + "OES_texture_float": true, + "OES_texture_half_float": true, + "OES_texture_half_float_linear": true, + "OES_vertex_array_object": true, + "WEBGL_color_buffer_float": true, + "WEBGL_compressed_texture_astc": true, + "WEBGL_compressed_texture_etc": true, + "WEBGL_compressed_texture_etc1": true, + "WEBGL_compressed_texture_pvrtc": true, + "WEBKIT_WEBGL_compressed_texture_pvrtc": true, + "WEBGL_debug_renderer_info": true, + "WEBGL_debug_shaders": true, + "WEBGL_depth_texture": true, + "WEBGL_draw_buffers": true, + "WEBGL_lose_context": true, + "WEBGL_multi_draw": true, + "WEBGL_polygon_mode": true + }, "peerconnection": true, - "datachannel": false, + "datachannel": true, + "getusermedia": true, + "mediastream": true, + "websockets": true, + "websocketsbinary": true, + "atobbtoa": true, + "atob-btoa": true, + "framed": false, "matchmedia": true, - "ligatures": true, - "cssanimations": true, - "csspseudoanimations": true, - "appearance": true, - "backdropfilter": true, - "backgroundcliptext": true, - "bgpositionxy": true, - "bgrepeatround": true, - "bgrepeatspace": true, - "backgroundsize": true, - "bgsizecover": true, - "borderimage": true, - "borderradius": true, - "boxshadow": true, - "boxsizing": true, - "csscolumns": true, - "cssgridlegacy": false, - "cssgrid": true, - "displayrunin": false, - "display-runin": false, - "ellipsis": true, - "cssfilters": true, - "flexbox": true, - "flexboxlegacy": true, - "flexboxtweener": false, - "flexwrap": true, - "cssmask": true, - "overflowscrolling": true, - "cssreflections": true, - "cssresize": true, - "scrollsnappoints": true, - "shapes": true, - "textalignlast": true, - "csstransforms": true, - "csstransforms3d": true, - "csstransformslevel2": true, - "csstransitions": true, - "csspseudotransitions": true, - "userselect": true, - "variablefonts": true + "pushmanager": false, + "resizeobserver": true, + "workertypeoption": true, + "sharedworkers": true, + "webworkers": true, + "transferables": true, + "xdomainrequest": false, + "devicemotion2": true, + "deviceorientation2": true, + "deviceorientation3": true } \ No newline at end of file diff --git a/tests/assets/modernizr/modernizr.js b/tests/assets/modernizr/modernizr.js new file mode 100644 index 0000000000..b8f4e2e65e --- /dev/null +++ b/tests/assets/modernizr/modernizr.js @@ -0,0 +1,4147 @@ +(()=>{var j=(e,A)=>()=>(A||e((A={exports:{}}).exports,A),A.exports);var Y=j((exports,module)=>{(function(scriptGlobalObject,window,document,undefined){var tests=[],ModernizrProto={_version:"4.0.0-alpha",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,A){var t=this;setTimeout(function(){A(t[e])},0)},addTest:function(e,A,t){tests.push({name:e,fn:A,options:t})},addAsyncTest:function(e){tests.push({name:null,fn:e})}},Modernizr=function(){};Modernizr.prototype=ModernizrProto,Modernizr=new Modernizr;var classes=[];function is(e,A){return typeof e===A}function testRunner(){var e,A,t,r,n,o,a;for(var l in tests)if(tests.hasOwnProperty(l)){if(e=[],A=tests[l],A.name&&(e.push(A.name.toLowerCase()),A.options&&A.options.aliases&&A.options.aliases.length))for(t=0;t0&&(A+=" "+t+e.join(" "+t)),isSVG?docElement.className.baseVal=A:docElement.className=A)}var hasOwnProp;(function(){var e={}.hasOwnProperty;!is(e,"undefined")&&!is(e.call,"undefined")?hasOwnProp=function(A,t){return e.call(A,t)}:hasOwnProp=function(A,t){return t in A&&is(A.constructor.prototype[t],"undefined")}})(),ModernizrProto._l={},ModernizrProto.on=function(e,A){this._l[e]||(this._l[e]=[]),this._l[e].push(A),Modernizr.hasOwnProperty(e)&&setTimeout(function(){Modernizr._trigger(e,Modernizr[e])},0)},ModernizrProto._trigger=function(e,A){if(this._l[e]){var t=this._l[e];setTimeout(function(){var r,n;for(r=0;r"u")return undefined;if(!e)return!1;if(e=e.replace(/^@/,""),r=e.replace(/-/g,"_").toUpperCase()+"_RULE",r in t)return"@"+e;for(var n=0;n",a="hidden"in d,u=d.childNodes.length==1||function(){A.createElement("a");var c=A.createDocumentFragment();return typeof c.cloneNode>"u"||typeof c.createDocumentFragment>"u"||typeof c.createElement>"u"}()}catch{a=!0,u=!0}})();function m(d,c){var p=d.createElement("p"),w=d.getElementsByTagName("head")[0]||d.documentElement;return p.innerHTML="x",w.insertBefore(p.lastChild,w.firstChild)}function g(){var d=h.elements;return typeof d=="string"?d.split(" "):d}function P(d,c){var p=h.elements;typeof p!="string"&&(p=p.join(" ")),typeof d!="string"&&(d=d.join(" ")),h.elements=p+" "+d,z(c)}function M(d){var c=f[d[l]];return c||(c={},s++,d[l]=s,f[s]=c),c}function b(d,c,p){if(c||(c=A),u)return c.createElement(d);p||(p=M(c));var w;return p.cache[d]?w=p.cache[d].cloneNode():o.test(d)?w=(p.cache[d]=p.createElem(d)).cloneNode():w=p.createElem(d),w.canHaveChildren&&!n.test(d)&&!w.tagUrn?p.frag.appendChild(w):w}function k(d,c){if(d||(d=A),u)return d.createDocumentFragment();c=c||M(d);for(var p=c.frag.cloneNode(),w=0,v=g(),E=v.length;w"u"||typeof A.parentWindow>"u"||typeof d.applyElement>"u"||typeof d.removeNode>"u"||typeof e.attachEvent>"u")}();function F(d){for(var c,p=d.getElementsByTagName("*"),w=p.length,v=RegExp("^(?:"+g().join("|")+")$","i"),E=[];w--;)c=p[w],v.test(c.nodeName)&&E.push(c.applyElement(W(c)));return E}function W(d){for(var c,p=d.attributes,w=p.length,v=d.ownerDocument.createElement(T+":"+d.nodeName);w--;)c=p[w],c.specified&&v.setAttribute(c.nodeName,c.nodeValue);return v.style.cssText=d.style.cssText,v}function G(d){for(var c,p=d.split("{"),w=p.length,v=RegExp("(^|[\\s,>+~])("+g().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),E="$1"+T+"\\:$2";w--;)c=p[w]=p[w].split("}"),c[c.length-1]=c[c.length-1].replace(v,E),p[w]=c.join("}");return p.join("{")}function Z(d){for(var c=d.length;c--;)d[c].removeNode()}function S(d){var c,p,w=M(d),v=d.namespaces,E=d.parentWindow;if(!V||d.printShived)return d;typeof v[T]>"u"&&v.add(T);function I(){clearTimeout(w._removeSheetTimer),c&&c.removeNode(!0),c=null}return E.attachEvent("onbeforeprint",function(){I();for(var Q,x,D,R=d.styleSheets,B=[],y=R.length,C=Array(y);y--;)C[y]=R[y];for(;D=C.pop();)if(!D.disabled&&U.test(D.media)){try{Q=D.imports,x=Q.length}catch{x=0}for(y=0;y7));Modernizr.addTest("csscalc",function(){var e="width:",A="calc(10px);",t=createElement("a");return t.style.cssText=e+prefixes.join(A+e),!!t.style.length});Modernizr.addTest("checked",function(){return testStyles("#modernizr {position:absolute} #modernizr input {margin-left:10px} #modernizr :checked {margin-left:20px;display:block}",function(e){var A=createElement("input");return A.setAttribute("type","checkbox"),A.setAttribute("checked","checked"),e.appendChild(A),A.offsetLeft===20})});Modernizr.addTest("csschunit",function(){var e=modElem.elem.style,A;try{e.fontSize="3ch",A=e.fontSize.indexOf("ch")!==-1}catch{A=!1}return A});(function(){Modernizr.addTest("csscolumns",function(){var n=!1,o=testAllProps("columnCount");try{n=!!o,n&&(n=new Boolean(n))}catch{}return n});for(var e=["Width","Span","Fill","Gap","Rule","RuleColor","RuleStyle","RuleWidth","BreakBefore","BreakAfter","BreakInside"],A,t,r=0;r9)});Modernizr.addTest("flexbox",testAllProps("flexBasis","1px",!0));Modernizr.addTest("flexboxlegacy",testAllProps("boxDirection","reverse",!0));Modernizr.addTest("flexboxtweener",testAllProps("flexAlign","end",!0));Modernizr.addTest("flexgap",function(){var e=createElement("div");e.style.display="flex",e.style.flexDirection="column",e.style.rowGap="1px",e.appendChild(createElement("div")),e.appendChild(createElement("div")),docElement.appendChild(e);var A=e.scrollHeight===1;return e.parentNode.removeChild(e),A});Modernizr.addTest("flexwrap",testAllProps("flexWrap","wrap",!0));Modernizr.addTest("focusvisible",function(){try{document.querySelector(":focus-visible")}catch{return!1}return!0});Modernizr.addTest("focuswithin",function(){try{document.querySelector(":focus-within")}catch{return!1}return!0});Modernizr.addTest("fontDisplay",testProp("font-display"));var unsupportedUserAgent=function(){var e=navigator.userAgent,A=e.match(/w(eb)?osbrowser/gi),t=e.match(/windows phone/gi)&&e.match(/iemobile\/([0-9])+/gi)&&parseFloat(RegExp.$1)>=9;return A||t}();unsupportedUserAgent?Modernizr.addTest("fontface",!1):testStyles('@font-face {font-family:"font";src:url("https://")}',function(e,A){var t=document.getElementById("smodernizr"),r=t.sheet||t.styleSheet,n=r?r.cssRules&&r.cssRules[0]?r.cssRules[0].cssText:r.cssText||"":"",o=/src/i.test(n)&&n.indexOf(A.split(" ")[0])===0;Modernizr.addTest("fontface",o)});testStyles('#modernizr{font:0/0 a}#modernizr:after{content:":)";visibility:hidden;font:7px/1 a}',function(e){Modernizr.addTest("generatedcontent",e.offsetHeight>=6)});Modernizr.addTest("cssgradients",function(){for(var e="background-image:",A="gradient(linear,left top,right bottom,from(#9f9),to(white));",t="",r,n=0,o=prefixes.length-1;n-1});Modernizr.addTest("hairline",function(){return testStyles("#modernizr {border:.5px solid transparent}",function(e){return e.offsetHeight===1})});Modernizr.addTest("hsla",function(){var e=createElement("a").style;return e.cssText="background-color:hsla(120,40%,100%,.5)",contains(e.backgroundColor,"rgba")||contains(e.backgroundColor,"hsla")});Modernizr.addAsyncTest(function(){var e=300;setTimeout(A,e);function A(){if(!document.body&&!document.getElementsByTagName("body")[0]){setTimeout(A,e);return}function t(){try{var o=createElement("div"),a=createElement("span"),l=o.style,s=0,f=0,u=!1,m=document.body.firstElementChild||document.body.firstChild;return o.lang="en",o.appendChild(a),a.innerHTML="Bacon ipsum dolor sit amet jerky velit in culpa hamburger et. Laborum dolor proident, enim dolore duis commodo et strip steak. Salami anim et, veniam consectetur dolore qui tenderloin jowl velit sirloin. Et ad culpa, fatback cillum jowl ball tip ham hock nulla short ribs pariatur aute. Pig pancetta ham bresaola, ut boudin nostrud commodo flank esse cow tongue culpa. Pork belly bresaola enim pig, ea consectetur nisi. Fugiat officia turkey, ea cow jowl pariatur ullamco proident do laborum velit sausage. Magna biltong sint tri-tip commodo sed bacon, esse proident aliquip. Ullamco ham sint fugiat, velit in enim sed mollit nulla cow ut adipisicing nostrud consectetur. Proident dolore beef ribs, laborum nostrud meatball ea laboris rump cupidatat labore culpa. Shankle minim beef, velit sint cupidatat fugiat tenderloin pig et ball tip. Ut cow fatback salami, bacon ball tip et in shank strip steak bresaola. In ut pork belly sed mollit tri-tip magna culpa veniam, short ribs qui in andouille ham consequat. Dolore bacon t-bone, velit short ribs enim strip steak nulla. Voluptate labore ut, biltong swine irure jerky. Cupidatat excepteur aliquip salami dolore. Ball tip strip steak in pork dolor. Ad in esse biltong. Dolore tenderloin exercitation ad pork loin t-bone, dolore in chicken ball tip qui pig. Ut culpa tongue, sint ribeye dolore ex shank voluptate hamburger. Jowl et tempor, boudin pork chop labore ham hock drumstick consectetur tri-tip elit swine meatball chicken ground round. Proident shankle mollit dolore. Shoulder ut duis t-bone quis reprehenderit. Meatloaf dolore minim strip steak, laboris ea aute bacon beef ribs elit shank in veniam drumstick qui. Ex laboris meatball cow tongue pork belly. Ea ball tip reprehenderit pig, sed fatback boudin dolore flank aliquip laboris eu quis. Beef ribs duis beef, cow corned beef adipisicing commodo nisi deserunt exercitation. Cillum dolor t-bone spare ribs, ham hock est sirloin. Brisket irure meatloaf in, boudin pork belly sirloin ball tip. Sirloin sint irure nisi nostrud aliqua. Nostrud nulla aute, enim officia culpa ham hock. Aliqua reprehenderit dolore sunt nostrud sausage, ea boudin pork loin ut t-bone ham tempor. Tri-tip et pancetta drumstick laborum. Ham hock magna do nostrud in proident. Ex ground round fatback, venison non ribeye in.",document.body.insertBefore(o,m),l.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justify:newspaper;",s=a.offsetHeight,f=a.offsetWidth,l.cssText="position:absolute;top:0;left:0;width:5em;text-align:justify;text-justify:newspaper;"+prefixes.join("hyphens:auto; "),u=a.offsetHeight!==s||a.offsetWidth!==f,document.body.removeChild(o),o.removeChild(a),u}catch{return!1}}function r(o,a){try{var l=createElement("div"),s=createElement("span"),f=l.style,u=0,m=!1,g=!1,P=!1,M=document.body.firstElementChild||document.body.firstChild;return f.cssText="position:absolute;top:0;left:0;overflow:visible;width:1.25em;",l.appendChild(s),document.body.insertBefore(l,M),s.innerHTML="mm",u=s.offsetHeight,s.innerHTML="m"+o+"m",g=s.offsetHeight>u,a?(s.innerHTML="m
m",u=s.offsetWidth,s.innerHTML="m"+o+"m",P=s.offsetWidth>u):P=!0,g===!0&&P===!0&&(m=!0),document.body.removeChild(l),l.removeChild(s),m}catch{return!1}}function n(o){try{var a=createElement("input"),l=createElement("div"),s="lebowski",f=!1,u,m=document.body.firstElementChild||document.body.firstChild;a.style.cssText="position:fixed;top:0;",l.style.cssText="position:fixed;top:0;",l.innerHTML=s+o+s,document.body.insertBefore(l,m),document.body.insertBefore(a,l),a.setSelectionRange?(a.focus(),a.setSelectionRange(0,0)):a.createTextRange&&(u=a.createTextRange(),u.collapse(!0),u.moveEnd("character",0),u.moveStart("character",0),u.select());try{window.find?f=window.find(s+s):(u=window.self.document.body.createTextRange(),f=u.findText(s+s))}catch{f=!1}return document.body.removeChild(l),document.body.removeChild(a),f}catch{return!1}}addTest("csshyphens",function(){if(!testAllProps("hyphens","auto",!0))return!1;try{return t()}catch{return!1}}),addTest("softhyphens",function(){try{return r("­",!0)&&r("​",!1)}catch{return!1}}),addTest("softhyphensfind",function(){try{return n("­")&&n("​")}catch{return!1}})}});Modernizr.addTest("cssinvalid",function(){return testStyles("#modernizr input{height:0;border:0;padding:0;margin:0;width:10px} #modernizr input:invalid{width:50px}",function(e){var A=createElement("input");return A.required=!0,e.appendChild(A),A.clientWidth>10})});testStyles("#modernizr div {width:100px} #modernizr :last-child{width:200px;display:block}",function(e){Modernizr.addTest("lastchild",e.lastChild.offsetWidth>e.firstChild.offsetWidth)},2);Modernizr.addTest("cssmask",testAllProps("maskRepeat","repeat-x",!0));Modernizr.addTest("mediaqueries",mq("only all"));Modernizr.addTest("multiplebgs",function(){var e=createElement("a").style;return e.cssText="background:url(https://),url(https://),red url(https://)",/(url\s*\(.*?){3}/.test(e.background)});testStyles("#modernizr div {width:1px} #modernizr div:nth-child(2n) {width:2px;}",function(e){var A=e.getElementsByTagName("div"),t=A[0].offsetWidth===A[2].offsetWidth&&A[1].offsetWidth===A[3].offsetWidth&&A[0].offsetWidth!==A[1].offsetWidth;Modernizr.addTest("nthchild",t)},4);Modernizr.addTest("objectfit",!!prefixed("objectFit"),{aliases:["object-fit"]});Modernizr.addTest("opacity",function(){var e=createElement("a").style;return e.cssText=prefixes.join("opacity:.55;"),/^0.55$/.test(e.opacity)});Modernizr.addTest("overflowscrolling",testAllProps("overflowScrolling","touch",!0));Modernizr.addTest("csspointerevents",function(){var e=createElement("a").style;return e.cssText="pointer-events:auto",e.pointerEvents==="auto"});Modernizr.addTest("csspositionsticky",function(){var e="position:",A="sticky",t=createElement("a"),r=t.style;return r.cssText=e+prefixes.join(A+";"+e).slice(0,-e.length),r.position.indexOf(A)!==-1});Modernizr.addTest("csspseudoanimations",function(){var e=!1;if(!Modernizr.cssanimations)return e;var A=["@",prefixes.join("keyframes csspseudoanimations { from { font-size: 10px; } }@").replace(/\@$/,""),'#modernizr:before { content:" "; font-size:5px;',prefixes.join("animation:csspseudoanimations 1ms infinite;"),"}"].join("");return testStyles(A,function(t){e=computedStyle(t,":before","font-size")==="10px"}),e});Modernizr.addTest("csstransitions",testAllProps("transition","all",!0));Modernizr.addTest("csspseudotransitions",function(){var e=!1;if(!Modernizr.csstransitions)return e;var A='#modernizr:before { content:" "; font-size:5px;'+prefixes.join("transition:0s 100s;")+"}#modernizr.trigger:before { font-size:10px; }";return testStyles(A,function(t){computedStyle(t,":before","font-size"),t.className+="trigger",e=computedStyle(t,":before","font-size")==="5px"}),e});Modernizr.addTest("cssreflections",testAllProps("boxReflect","above",!0));Modernizr.addTest("regions",function(){if(isSVG)return!1;var e=prefixed("flowFrom"),A=prefixed("flowInto"),t=!1;if(!e||!A)return t;var r=createElement("iframe"),n=createElement("div"),o=createElement("div"),a=createElement("div"),l="modernizr_flow_for_regions_check";o.innerText="M",n.style.cssText="top: 150px; left: 150px; padding: 0px;",a.style.cssText="width: 50px; height: 50px; padding: 42px;",a.style[e]=l,n.appendChild(o),n.appendChild(a),docElement.appendChild(n);var s,f,u=o.getBoundingClientRect();return o.style[A]=l,s=o.getBoundingClientRect(),f=parseInt(s.left-u.left,10),docElement.removeChild(n),f===42?t=!0:(docElement.appendChild(r),u=r.getBoundingClientRect(),r.style[A]=l,s=r.getBoundingClientRect(),u.height>0&&u.height!==s.height&&s.height===0&&(t=!0)),o=a=n=r=undefined,t});Modernizr.addTest("cssremunit",function(){var e=createElement("a").style;try{e.fontSize="3rem"}catch{}return/rem/.test(e.fontSize)});Modernizr.addTest("cssresize",testAllProps("resize","both",!0));Modernizr.addTest("rgba",function(){var e=createElement("a").style;return e.cssText="background-color:rgba(150,255,150,.5)",(""+e.backgroundColor).indexOf("rgba")>-1});testStyles("#modernizr{overflow: scroll; width: 40px; height: 40px; }#"+prefixes.join("scrollbar{width:10px} #modernizr::").split("#").slice(1).join("#")+"scrollbar{width:10px}",function(e){Modernizr.addTest("cssscrollbar","scrollWidth"in e&&e.scrollWidth===30)});Modernizr.addTest("scrollsnappoints",testAllProps("scrollSnapType"));Modernizr.addTest("shapes",testAllProps("shapeOutside","content-box",!0));Modernizr.addTest("siblinggeneral",function(){return testStyles("#modernizr div {width:100px} #modernizr div ~ div {width:200px;display:block}",function(e){return e.lastChild.offsetWidth===200},2)});testStyles("#modernizr{position: absolute; top: -10em; visibility:hidden; font: normal 10px arial;}#subpixel{float: left; font-size: 33.3333%;}",function(e){var A=e.firstChild;A.innerHTML="This is a text written in Arial",Modernizr.addTest("subpixelfont",computedStyle(A,null,"width")!=="44px")},1,["subpixel"]);Modernizr.addTest("target",function(){var e=window.document;if(!("querySelectorAll"in e))return!1;try{return e.querySelectorAll(":target"),!0}catch{return!1}});Modernizr.addTest("textalignlast",testAllProps("textAlignLast"));(function(){Modernizr.addTest("textdecoration",function(){var n=!1,o=testAllProps("textDecoration");try{n=!!o,n&&(n=new Boolean(n))}catch{}return n});for(var e=["Line","Style","Color","Skip","SkipInk"],A,t,r=0;r10})});Modernizr.addTest("variablefonts",testAllProps("fontVariationSettings"));testStyles("#modernizr { height: 50vh; max-height: 10px; }",function(e){var A=parseInt(computedStyle(e,null,"height"),10);Modernizr.addTest("cssvhunit",A===10)});function roundedEquals(e,A){return e-1===A||e===A||e+1===A}testStyles("#modernizr1{width: 50vmax}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(e){var A=e.childNodes[2],t=e.childNodes[1],r=e.childNodes[0],n=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=r.clientWidth/100,a=r.clientHeight/100,l=parseInt(Math.max(o,a)*50,10),s=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvmaxunit",roundedEquals(l,s)||roundedEquals(l,s-n))},3);testStyles("#modernizr1{width: 50vm;width:50vmin}#modernizr2{width:50px;height:50px;overflow:scroll}#modernizr3{position:fixed;top:0;left:0;bottom:0;right:0}",function(e){var A=e.childNodes[2],t=e.childNodes[1],r=e.childNodes[0],n=parseInt((t.offsetWidth-t.clientWidth)/2,10),o=r.clientWidth/100,a=r.clientHeight/100,l=parseInt(Math.min(o,a)*50,10),s=parseInt(computedStyle(A,null,"width"),10);Modernizr.addTest("cssvminunit",roundedEquals(l,s)||roundedEquals(l,s-n))},3);testStyles("#modernizr { width: 50vw; }",function(e){var A=parseInt(window.innerWidth/2,10),t=parseInt(computedStyle(e,null,"width"),10);Modernizr.addTest("cssvwunit",roundedEquals(t,A))});Modernizr.addTest("willchange","willChange"in docElement.style);Modernizr.addTest("wrapflow",function(){var e=prefixed("wrapFlow");if(!e||isSVG)return!1;var A=e.replace(/([A-Z])/g,function(a,l){return"-"+l.toLowerCase()}).replace(/^ms-/,"-ms-"),t=createElement("div"),r=createElement("div"),n=createElement("span");r.style.cssText="position: absolute; left: 50px; width: 100px; height: 20px;"+A+":end;",n.innerText="X",t.appendChild(r),t.appendChild(n),docElement.appendChild(t);var o=n.offsetLeft;return docElement.removeChild(t),r=n=t=undefined,o===150});Modernizr.addTest("customelements","customElements"in window);Modernizr.addTest("customprotocolhandler",function(){if(!navigator.registerProtocolHandler)return!1;try{navigator.registerProtocolHandler("thisShouldFail")}catch(e){return e instanceof TypeError}return!1});Modernizr.addTest("dart",!!prefixed("startDart",navigator));Modernizr.addTest("dataview",typeof DataView<"u"&&"getFloat64"in DataView.prototype);Modernizr.addTest("classlist","classList"in docElement);Modernizr.addTest("createelementattrs",function(){try{return createElement('').getAttribute("name")==="test"}catch{return!1}},{aliases:["createelement-attrs"]});Modernizr.addTest("dataset",function(){var e=createElement("div");return e.setAttribute("data-a-b","c"),!!(e.dataset&&e.dataset.aB==="c")});Modernizr.addTest("documentfragment",function(){return"createDocumentFragment"in document&&"appendChild"in docElement});Modernizr.addTest("hidden","hidden"in createElement("a"));Modernizr.addTest("intersectionobserver","IntersectionObserver"in window);Modernizr.addTest("microdata","getItems"in document);Modernizr.addTest("mutationobserver",!!window.MutationObserver||!!window.WebKitMutationObserver);Modernizr.addTest("passiveeventlisteners",function(){var e=!1;try{var A=Object.defineProperty({},"passive",{get:function(){e=!0}}),t=function(){};window.addEventListener("testPassiveEventSupport",t,A),window.removeEventListener("testPassiveEventSupport",t,A)}catch{}return e});Modernizr.addTest("shadowroot","attachShadow"in createElement("div"));Modernizr.addTest("shadowrootlegacy","createShadowRoot"in createElement("div"));Modernizr.addTest("bdi",function(){var e=createElement("div"),A=createElement("bdi");A.innerHTML="إ",e.appendChild(A),docElement.appendChild(e);var t=computedStyle(A,null,"direction")==="rtl";return docElement.removeChild(e),t});Modernizr.addTest("details",function(){var e=createElement("details"),A;return"open"in e?(testStyles("#modernizr details{display:block}",function(t){t.appendChild(e),e.innerHTML="ab",A=e.offsetHeight,e.open=!0,A=A!==e.offsetHeight}),A):!1});Modernizr.addTest("outputelem","value"in createElement("output"));Modernizr.addTest("picture","HTMLPictureElement"in window);Modernizr.addTest("progressbar",createElement("progress").max!==undefined),Modernizr.addTest("meter",createElement("meter").max!==undefined);Modernizr.addTest("ruby",function(){var e=createElement("ruby"),A=createElement("rt"),t=createElement("rp");if(e.appendChild(t),e.appendChild(A),docElement.appendChild(e),computedStyle(t,null,"display")==="none"||computedStyle(e,null,"display")==="ruby"&&computedStyle(A,null,"display")==="ruby-text"||computedStyle(t,null,"fontSize")==="6pt"&&computedStyle(A,null,"fontSize")==="6pt")return r(),!0;return r(),!1;function r(){docElement.removeChild(e),e=null,A=null,t=null}});Modernizr.addTest("template","content"in createElement("template"));Modernizr.addTest("time","valueAsDate"in createElement("time"));Modernizr.addTest("texttrackapi",typeof createElement("video").addTextTrack=="function"),Modernizr.addTest("track","kind"in createElement("track"));Modernizr.addTest("unknownelements",function(){var e=createElement("a");return e.innerHTML="",e.childNodes.length===1});Modernizr.addTest("emoji",function(){if(!Modernizr.canvastext)return!1;var e=createElement("canvas"),A=e.getContext("2d"),t=A.webkitBackingStorePixelRatio||A.mozBackingStorePixelRatio||A.msBackingStorePixelRatio||A.oBackingStorePixelRatio||A.backingStorePixelRatio||1,r=12*t;return A.fillStyle="#f00",A.textBaseline="top",A.font="32px Arial",A.fillText("\u{1F428}",0,0),A.getImageData(r,r,1,1).data[0]!==0});Modernizr.addTest("es5array",function(){return!!(Array.prototype&&Array.prototype.every&&Array.prototype.filter&&Array.prototype.forEach&&Array.prototype.indexOf&&Array.prototype.lastIndexOf&&Array.prototype.map&&Array.prototype.some&&Array.prototype.reduce&&Array.prototype.reduceRight&&Array.isArray)});Modernizr.addTest("es5date",function(){var e="2013-04-12T06:06:37.307Z",A=!1;try{A=!!Date.parse(e)}catch{}return!!(Date.now&&Date.prototype&&Date.prototype.toISOString&&Date.prototype.toJSON&&A)});Modernizr.addTest("es5function",function(){return!!(Function.prototype&&Function.prototype.bind)});Modernizr.addTest("es5object",function(){return!!(Object.keys&&Object.create&&Object.getPrototypeOf&&Object.getOwnPropertyNames&&Object.isSealed&&Object.isFrozen&&Object.isExtensible&&Object.getOwnPropertyDescriptor&&Object.defineProperty&&Object.defineProperties&&Object.seal&&Object.freeze&&Object.preventExtensions)});Modernizr.addTest("strictmode",function(){"use strict";return!this}());Modernizr.addTest("es5string",function(){return!!(String.prototype&&String.prototype.trim)});Modernizr.addTest("json","JSON"in window&&"parse"in JSON&&"stringify"in JSON);Modernizr.addTest("es5syntax",function(){var value,obj,stringAccess,getter,setter,reservedWords,zeroWidthChars;try{return stringAccess=eval('"foobar"[3] === "b"'),getter=eval("({ get x(){ return 1 } }).x === 1"),eval("({ set x(v){ value = v; } }).x = 1"),setter=value===1,eval("obj = ({ if: 1 })"),reservedWords=obj.if===1,zeroWidthChars=eval("_\u200C\u200D = true"),stringAccess&&getter&&setter&&reservedWords&&zeroWidthChars}catch(e){return!1}});Modernizr.addTest("es5undefined",function(){var e,A;try{A=window.undefined,window.undefined=12345,e=typeof window.undefined>"u",window.undefined=A}catch{return!1}return e});Modernizr.addTest("es5",function(){return!!(Modernizr.es5array&&Modernizr.es5date&&Modernizr.es5function&&Modernizr.es5object&&Modernizr.strictmode&&Modernizr.es5string&&Modernizr.json&&Modernizr.es5syntax&&Modernizr.es5undefined)});Modernizr.addTest("es6array",!!(Array.prototype&&Array.prototype.copyWithin&&Array.prototype.fill&&Array.prototype.find&&Array.prototype.findIndex&&Array.prototype.keys&&Array.prototype.entries&&Array.prototype.values&&Array.from&&Array.of));Modernizr.addTest("arrow",function(){try{eval("()=>{}")}catch(e){return!1}return!0});Modernizr.addTest("es6class",function(){try{eval("class A{}")}catch(e){return!1}return!0});Modernizr.addTest("es6collections",!!(window.Map&&window.Set&&window.WeakMap&&window.WeakSet));Modernizr.addTest("generators",function(){try{new Function("function* test() {}")()}catch{return!1}return!0});Modernizr.addTest("es6math",!!(Math&&Math.clz32&&Math.cbrt&&Math.imul&&Math.sign&&Math.log10&&Math.log2&&Math.log1p&&Math.expm1&&Math.cosh&&Math.sinh&&Math.tanh&&Math.acosh&&Math.asinh&&Math.atanh&&Math.hypot&&Math.trunc&&Math.fround));Modernizr.addTest("es6number",!!(Number.isFinite&&Number.isInteger&&Number.isSafeInteger&&Number.isNaN&&Number.parseInt&&Number.parseFloat&&Number.isInteger(Number.MAX_SAFE_INTEGER)&&Number.isInteger(Number.MIN_SAFE_INTEGER)&&Number.isFinite(Number.EPSILON)));Modernizr.addTest("es6object",!!(Object.assign&&Object.is&&Object.setPrototypeOf));Modernizr.addTest("promises",function(){return"Promise"in window&&"resolve"in window.Promise&&"reject"in window.Promise&&"all"in window.Promise&&"race"in window.Promise&&function(){var e;return new window.Promise(function(A){e=A}),typeof e=="function"}()});Modernizr.addTest("restparameters",function(){try{eval("function f(...rest) {}")}catch(e){return!1}return!0});Modernizr.addTest("spreadarray",function(){try{eval("(function f(){})(...[1])")}catch(e){return!1}return!0});Modernizr.addTest("stringtemplate",function(){try{return eval("(function(){var a=1; return `-${a}-`;})()")==="-1-"}catch(e){return!1}});Modernizr.addTest("es6string",!!(String.fromCodePoint&&String.raw&&String.prototype.codePointAt&&String.prototype.repeat&&String.prototype.startsWith&&String.prototype.endsWith&&String.prototype.includes));Modernizr.addTest("es6symbol",!!(typeof Symbol=="function"&&Symbol.for&&Symbol.hasInstance&&Symbol.isConcatSpreadable&&Symbol.iterator&&Symbol.keyFor&&Symbol.match&&Symbol.prototype&&Symbol.replace&&Symbol.search&&Symbol.species&&Symbol.split&&Symbol.toPrimitive&&Symbol.toStringTag&&Symbol.unscopables));Modernizr.addTest("es7array",!!(Array.prototype&&Array.prototype.includes));Modernizr.addTest("restdestructuringarray",function(){try{eval("var [...rest]=[1]")}catch(e){return!1}return!0}),Modernizr.addTest("restdestructuringobject",function(){try{eval("var {...rest}={a:1}")}catch(e){return!1}return!0});Modernizr.addTest("spreadobject",function(){try{eval("var a={...{b:1}}")}catch(e){return!1}return!0});Modernizr.addTest("es8object",!!(Object.entries&&Object.values));Modernizr.addTest("customevent","CustomEvent"in window&&typeof window.CustomEvent=="function");Modernizr.addTest("devicemotion","DeviceMotionEvent"in window),Modernizr.addTest("deviceorientation","DeviceOrientationEvent"in window);Modernizr.addTest("eventlistener","addEventListener"in window);Modernizr.addTest("forcetouch",function(){return hasEvent(prefixed("mouseforcewillbegin",window,!1),window)?MouseEvent.WEBKIT_FORCE_AT_MOUSE_DOWN&&MouseEvent.WEBKIT_FORCE_AT_FORCE_MOUSE_DOWN:!1});Modernizr.addTest("hashchange",function(){return hasEvent("hashchange",window)===!1?!1:document.documentMode===undefined||document.documentMode>7});Modernizr.addTest("oninput",function(){var e=createElement("input"),A;if(e.setAttribute("oninput","return"),e.style.cssText="position:fixed;top:0;",hasEvent("oninput",docElement)||typeof e.oninput=="function")return!0;try{var t=document.createEvent("KeyboardEvent");A=!1;var r=function(n){A=!0,n.preventDefault(),n.stopPropagation()};t.initKeyEvent("keypress",!0,!0,window,!1,!1,!1,!1,0,"e".charCodeAt(0)),docElement.appendChild(e),e.addEventListener("input",r,!1),e.focus(),e.dispatchEvent(t),e.removeEventListener("input",r,!1),docElement.removeChild(e)}catch{A=!1}return A});var domPrefixesAll=[""].concat(domPrefixes);ModernizrProto._domPrefixesAll=domPrefixesAll;Modernizr.addTest("pointerevents",function(){for(var e=0,A=domPrefixesAll.length;e"u"?!1:(t.drawImage(e,0,0),t.getImageData(0,0,1,1).data[3]===0)})},e.src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACGFjVEwAAAABAAAAAcMq2TYAAAANSURBVAiZY2BgYPgPAAEEAQB9ssjfAAAAGmZjVEwAAAAAAAAAAQAAAAEAAAAAAAAAAAD6A+gBAbNU+2sAAAARZmRBVAAAAAEImWNgYGBgAAAABQAB6MzFdgAAAABJRU5ErkJggg=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("avif",e.width===1)},e.src="data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAAEcbWV0YQAAAAAAAABIaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGNhdmlmIC0gaHR0cHM6Ly9naXRodWIuY29tL2xpbmstdS9jYXZpZgAAAAAeaWxvYwAAAAAEQAABAAEAAAAAAUQAAQAAABcAAAAqaWluZgEAAAAAAAABAAAAGmluZmUCAAAAAAEAAGF2MDFJbWFnZQAAAAAOcGl0bQAAAAAAAQAAAHJpcHJwAAAAUmlwY28AAAAQcGFzcAAAAAEAAAABAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAQcGl4aQAAAAADCAgIAAAAFmF2MUOBAAwACggYAAYICGgIIAAAABhpcG1hAAAAAAAAAAEAAQUBAoMDhAAAAB9tZGF0CggYAAYICGgIIBoFHiAAAEQiBACwDoA="});Modernizr.addTest("imgcrossorigin","crossOrigin"in createElement("img"));Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("exiforientation",!1,{aliases:["exif-orientation"]})},e.onload=function(){addTest("exiforientation",e.width!==2,{aliases:["exif-orientation"]})},e.src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/4QAiRXhpZgAASUkqAAgAAAABABIBAwABAAAABgASAAAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAIDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD+/iiiigD/2Q=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("jpeg2000",e.width===1)},e.src="data:image/jp2;base64,/0//UQAyAAAAAAABAAAAAgAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAEBwEBBwEBBwEBBwEB/1IADAAAAAEAAAQEAAH/XAAEQED/ZAAlAAFDcmVhdGVkIGJ5IE9wZW5KUEVHIHZlcnNpb24gMi4wLjD/kAAKAAAAAABYAAH/UwAJAQAABAQAAf9dAAUBQED/UwAJAgAABAQAAf9dAAUCQED/UwAJAwAABAQAAf9dAAUDQED/k8+kEAGvz6QQAa/PpBABr994EAk//9k="});Modernizr.addAsyncTest(function(){var e=new Image;e.onload=e.onerror=function(){addTest("jpegxr",e.width===1,{aliases:["jpeg-xr"]})},e.src="data:image/vnd.ms-photo;base64,SUm8AQgAAAAFAAG8AQAQAAAASgAAAIC8BAABAAAAAQAAAIG8BAABAAAAAQAAAMC8BAABAAAAWgAAAMG8BAABAAAAHwAAAAAAAAAkw91vA07+S7GFPXd2jckNV01QSE9UTwAZAYBxAAAAABP/gAAEb/8AAQAAAQAAAA=="});Modernizr.addTest("lazyloading","loading"in HTMLImageElement.prototype);Modernizr.addAsyncTest(function(){var e,A,t,r=createElement("img"),n="sizes"in r;!n&&"srcset"in r?(A="data:image/gif;base64,R0lGODlhAgABAPAAAP///wAAACH5BAAAAAAALAAAAAACAAEAAAICBAoAOw==",e="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",t=function(){addTest("sizes",r.width===2)},r.onload=t,r.onerror=t,r.setAttribute("sizes","9px"),r.srcset=e+" 1w,"+A+" 8w",r.src=e):addTest("sizes",n)});Modernizr.addTest("srcset","srcset"in createElement("img"));Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webpalpha",!1,{aliases:["webp-alpha"]})},e.onload=function(){addTest("webpalpha",e.width===1,{aliases:["webp-alpha"]})},e.src="data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA=="});Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webpanimation",!1,{aliases:["webp-animation"]})},e.onload=function(){addTest("webpanimation",e.width===1,{aliases:["webp-animation"]})},e.src="data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA"});Modernizr.addAsyncTest(function(){var e=new Image;e.onerror=function(){addTest("webplossless",!1,{aliases:["webp-lossless"]})},e.onload=function(){addTest("webplossless",e.width===1,{aliases:["webp-lossless"]})},e.src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="});Modernizr.addAsyncTest(function(){var e=[{uri:"data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA=",name:"webp"},{uri:"data:image/webp;base64,UklGRkoAAABXRUJQVlA4WAoAAAAQAAAAAAAAAAAAQUxQSAwAAAABBxAR/Q9ERP8DAABWUDggGAAAADABAJ0BKgEAAQADADQlpAADcAD++/1QAA==",name:"webp.alpha"},{uri:"data:image/webp;base64,UklGRlIAAABXRUJQVlA4WAoAAAASAAAAAAAAAAAAQU5JTQYAAAD/////AABBTk1GJgAAAAAAAAAAAAAAAAAAAGQAAABWUDhMDQAAAC8AAAAQBxAREYiI/gcA",name:"webp.animation"},{uri:"data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=",name:"webp.lossless"}],A=e.shift();function t(r,n,o){var a=new Image;function l(s){var f=s&&s.type==="load"?a.width===1:!1,u=r==="webp";addTest(r,u&&f?new Boolean(f):f),o&&o(s)}a.onerror=l,a.onload=l,a.src=n}t(A.name,A.uri,function(r){if(r&&r.type==="load")for(var n=0;n',testStyles("#modernizr form{position:absolute;top:-99999em}",function(r){r.appendChild(e),t=e.getElementsByTagName("input")[0],t.addEventListener("invalid",function(n){A=!0,n.preventDefault(),n.stopPropagation()},!1),Modernizr.formvalidationmessage=!!t.validationMessage,e.getElementsByTagName("button")[0].click()}),A});Modernizr.addTest("localizednumber",function(){if(!Modernizr.inputtypes.number||!Modernizr.formvalidation)return!1;var e=getBody(),A=createElement("div"),t=e.firstElementChild||e.firstChild,r;e.insertBefore(A,t),A.innerHTML='';var n=A.childNodes[0];e.appendChild(A),n.focus();try{document.execCommand("SelectAll",!1),document.execCommand("InsertText",!1,"1,1")}catch{}return r=n.type==="number"&&n.valueAsNumber===1.1&&n.checkValidity(),e.removeChild(A),e.fake&&e.parentNode&&e.parentNode.removeChild(e),r});Modernizr.addTest("inputsearchevent",hasEvent("search"));Modernizr.addTest("placeholder","placeholder"in createElement("input")&&"placeholder"in createElement("textarea"));Modernizr.addTest("requestautocomplete",!!prefixed("requestAutocomplete",createElement("form")));Modernizr.addTest("intl",!!prefixed("Intl",window));Modernizr.addTest("ligatures",testAllProps("fontFeatureSettings",'"liga" 1'));Modernizr.addTest("olreversed","reversed"in createElement("ol"));Modernizr.addTest("mathml",function(){var e;return testStyles("#modernizr{position:absolute;display:inline-block}",function(A){A.innerHTML+="xxyy",e=A.offsetHeight>A.offsetWidth}),e});Modernizr.addTest("mediasource","MediaSource"in window);Modernizr.addTest("hovermq",mq("(hover)"));Modernizr.addTest("pointermq",mq("(pointer:coarse),(pointer:fine),(pointer:none)"));Modernizr.addTest("messagechannel","MessageChannel"in window);Modernizr.addTest("beacon","sendBeacon"in navigator);Modernizr.addTest("effectiveType",function(){var e=navigator.connection||{effectiveType:0};return e.effectiveType!==0});Modernizr.addTest("lowbandwidth",function(){var e=navigator.connection||{type:0,effectiveType:0};return e.type===3||e.type===4||/^[23]g$/.test(e.effectiveType)});Modernizr.addTest("eventsource","EventSource"in window);Modernizr.addTest("fetch","fetch"in window);var testXhrType=function(e){if(typeof XMLHttpRequest>"u")return!1;var A=new XMLHttpRequest;A.open("get","/",!0);try{A.responseType=e}catch{return!1}return"response"in A&&A.responseType===e};Modernizr.addTest("xhrresponsetypearraybuffer",testXhrType("arraybuffer"));Modernizr.addTest("xhrresponsetypeblob",testXhrType("blob"));Modernizr.addTest("xhrresponsetypedocument",testXhrType("document"));Modernizr.addTest("xhrresponsetypejson",testXhrType("json"));Modernizr.addTest("xhrresponsetypetext",testXhrType("text"));Modernizr.addTest("xhrresponsetype",function(){if(typeof XMLHttpRequest>"u")return!1;var e=new XMLHttpRequest;return e.open("get","/",!0),"response"in e}());Modernizr.addTest("xhr2","XMLHttpRequest"in window&&"withCredentials"in new XMLHttpRequest);Modernizr.addTest("notification",function(){if(!window.Notification||!window.Notification.requestPermission)return!1;if(window.Notification.permission==="granted")return!0;try{new window.Notification("")}catch(e){if(e.name==="TypeError")return!1}return!0});Modernizr.addTest("pagevisibility",!!prefixed("hidden",document,!1));Modernizr.addTest("performance",!!prefixed("performance",window));Modernizr.addTest("pointerlock",!!prefixed("exitPointerLock",document));var bool=!0;try{window.postMessage({toString:function(){bool=!1}},"*")}catch(e){}Modernizr.addTest("postmessage",new Boolean("postMessage"in window)),Modernizr.addTest("postmessage.structuredclones",bool);Modernizr.addTest("proxy","Proxy"in window);Modernizr.addTest("queryselector","querySelector"in document&&"querySelectorAll"in document);Modernizr.addTest("prefetch",function(){if(document.documentMode===11)return!0;var e=createElement("link").relList;return!e||!e.supports?!1:e.supports("prefetch")});Modernizr.addTest("requestanimationframe",!!prefixed("requestAnimationFrame",window),{aliases:["raf"]});Modernizr.addTest("scriptasync","async"in createElement("script"));Modernizr.addTest("scriptdefer","defer"in createElement("script"));Modernizr.addTest("scrolltooptions",function(){var e=getBody(),A=window.pageYOffset,t=e.clientHeight<=window.innerHeight;if(t){var r=createElement("div");r.style.height=window.innerHeight-e.clientHeight+1+"px",r.style.display="block",e.appendChild(r)}window.scrollTo({top:1});var n=window.pageYOffset!==0;return t&&e.removeChild(r),window.scrollTo(0,A),n});Modernizr.addTest("serviceworker","serviceWorker"in navigator);Modernizr.addTest("speechrecognition",function(){try{return!!prefixed("SpeechRecognition",window)}catch{return!1}});Modernizr.addTest("speechsynthesis",function(){try{return"SpeechSynthesisUtterance"in window}catch{return!1}});Modernizr.addTest("cookies",function(){try{document.cookie="cookietest=1";var e=document.cookie.indexOf("cookietest=")!==-1;return document.cookie="cookietest=1; expires=Thu, 01-Jan-1970 00:00:01 GMT",e}catch{return!1}});Modernizr.addAsyncTest(function(){var e;try{e=prefixed("indexedDB",window)}catch{}if(e){var A="modernizr-"+Math.random(),t;try{t=e.open(A)}catch{addTest("indexeddb",!1);return}t.onerror=function(r){t.error&&(t.error.name==="InvalidStateError"||t.error.name==="UnknownError")?(addTest("indexeddb",!1),r.preventDefault()):(addTest("indexeddb",!0),detectDeleteDatabase(e,A))},t.onsuccess=function(){addTest("indexeddb",!0),detectDeleteDatabase(e,A)}}else addTest("indexeddb",!1)});function detectDeleteDatabase(e,A){var t=e.deleteDatabase(A);t.onsuccess=function(){addTest("indexeddb.deletedatabase",!0)},t.onerror=function(){addTest("indexeddb.deletedatabase",!1)}}Modernizr.addAsyncTest(function(){var e,A="detect-blob-support",t=!1,r,n,o;try{e=prefixed("indexedDB",window)}catch{}if(!(Modernizr.indexeddb&&Modernizr.indexeddb.deletedatabase))return!1;try{e.deleteDatabase(A).onsuccess=function(){r=e.open(A,1),r.onupgradeneeded=function(){r.result.createObjectStore("store")},r.onsuccess=function(){n=r.result;try{o=n.transaction("store","readwrite").objectStore("store").put(new Blob,"key"),o.onsuccess=function(){t=!0},o.onerror=function(){t=!1}}catch{t=!1}finally{addTest("indexeddbblob",t),n.close(),e.deleteDatabase(A)}}}}catch{addTest("indexeddbblob",!1)}});Modernizr.addAsyncTest(function(){Modernizr.on("indexeddb",function(e){e&&addTest("indexeddb2","getAll"in IDBIndex.prototype)})});Modernizr.addTest("localstorage",function(){var e="modernizr";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch{return!1}});Modernizr.addTest("quotamanagement",function(){var e=prefixed("temporaryStorage",navigator),A=prefixed("persistentStorage",navigator);return!!(e&&A)});Modernizr.addTest("sessionstorage",function(){var e="modernizr";try{return sessionStorage.setItem(e,e),sessionStorage.removeItem(e),!0}catch{return!1}});Modernizr.addTest("userdata",!!createElement("div").addBehavior);Modernizr.addTest("websqldatabase","openDatabase"in window);Modernizr.addTest("stylescoped","scoped"in createElement("style"));Modernizr.addTest("svg",!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect);Modernizr.addTest("svgasimg",document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1"));var toStringFn={}.toString;Modernizr.addTest("svgclippaths",function(){return!!document.createElementNS&&/SVGClipPath/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","clipPath")))});Modernizr.addTest("svgfilters",function(){var e=!1;try{e="SVGFEColorMatrixElement"in window&&SVGFEColorMatrixElement.SVG_FECOLORMATRIX_TYPE_SATURATE===2}catch{}return e});Modernizr.addTest("svgforeignobject",function(){return!!document.createElementNS&&/SVGForeignObject/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","foreignObject")))});Modernizr.addTest("inlinesvg",function(){var e=createElement("div");return e.innerHTML="",(typeof SVGRect<"u"&&e.firstChild&&e.firstChild.namespaceURI)==="http://www.w3.org/2000/svg"});Modernizr.addTest("smil",function(){return!!document.createElementNS&&/SVGAnimate/.test(toStringFn.call(document.createElementNS("http://www.w3.org/2000/svg","animate")))});Modernizr.addTest("textareamaxlength","maxLength"in createElement("textarea"));Modernizr.addTest("textencoder",!!(window.TextEncoder&&window.TextEncoder.prototype.encode)),Modernizr.addTest("textdecoder",!!(window.TextDecoder&&window.TextDecoder.prototype.decode));Modernizr.addTest("typedarrays","ArrayBuffer"in window);Modernizr.addTest("unicoderange",function(){return testStyles('@font-face{font-family:"unicodeRange";src:local("Arial");unicode-range:U+0020,U+002E}#modernizr span{font-size:20px;display:inline-block;font-family:"unicodeRange",monospace}#modernizr .mono{font-family:monospace}',function(e){for(var A=[".",".","m","m"],t=0;t 1 due to Webkit [bug #45761](https://bugs.webkit.org/show_bug.cgi?id=45761)"], + "notes": [{ + "name": "Comprehensive Compat Chart", + "href": "https://muddledramblings.com/table-of-css3-border-radius-compliance/" + }] +} +!*/ +/*! +{ + "name": "CSS Custom Properties", + "property": "customproperties", + "caniuse": "css-variables", + "tags": ["css"], + "builderAliases": ["css_customproperties"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/--*" + }, { + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-variables/" + }] +} +!*/ +/*! +{ + "name": "CSS Display run-in", + "property": "display-runin", + "authors": ["alanhogan"], + "tags": ["css"], + "builderAliases": ["css_displayrunin"], + "notes": [{ + "name": "CSS Tricks Article", + "href": "https://web.archive.org/web/20111204150927/http://css-tricks.com:80/596-run-in/" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/198" + }] +} +!*/ +/*! +{ + "name": "CSS Display table", + "property": "displaytable", + "caniuse": "css-table", + "authors": ["scottjehl"], + "tags": ["css"], + "builderAliases": ["css_displaytable"], + "notes": [{ + "name": "Detects for all additional table display values", + "href": "https://pastebin.com/Gk9PeVaQ" + }] +} +!*/ +/*! +{ + "name": "CSS text-overflow ellipsis", + "property": "ellipsis", + "caniuse": "text-overflow", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS.escape()", + "property": "cssescape", + "polyfills": ["css-escape"], + "tags": ["css", "cssom"] +} +!*/ +/*! +{ + "name": "CSS Font ex Units", + "authors": ["Ron Waldon (@jokeyrhyme)"], + "property": "cssexunit", + "caniuse": "mdn-css_types_length_ex", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-values/#font-relative-lengths" + }] +} +!*/ +/*! +{ + "name": "CSS Supports", + "property": "supports", + "caniuse": "css-featurequeries", + "tags": ["css"], + "builderAliases": ["css_supports"], + "notes": [{ + "name": "W3C Spec (The @supports rule)", + "href": "https://dev.w3.org/csswg/css3-conditional/#at-supports" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/648" + }, { + "name": "W3C Spec (The CSSSupportsRule interface)", + "href": "https://dev.w3.org/csswg/css3-conditional/#the-csssupportsrule-interface" + }] +} +!*/ +/*! +{ + "name": "CSS Filters", + "property": "cssfilters", + "caniuse": "css-filters", + "polyfills": ["polyfilter"], + "tags": ["css"], + "builderAliases": ["css_filters"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/filter" + }] +} +!*/ +/*! +{ + "name": "Flexbox", + "property": "flexbox", + "caniuse": "flexbox", + "tags": ["css"], + "notes": [{ + "name": "The _new_ flexbox", + "href": "https://www.w3.org/TR/css-flexbox-1/" + }], + "warnings": [ + "A `true` result for this detect does not imply that the `flex-wrap` property is supported; see the `flexwrap` detect." + ] +} +!*/ +/*! +{ + "name": "Flexbox (legacy)", + "property": "flexboxlegacy", + "tags": ["css"], + "polyfills": ["flexie"], + "notes": [{ + "name": "The _old_ flexbox", + "href": "https://www.w3.org/TR/2009/WD-css3-flexbox-20090723/" + }] +} +!*/ +/*! +{ + "name": "Flexbox (tweener)", + "property": "flexboxtweener", + "tags": ["css"], + "polyfills": ["flexie"], + "notes": [{ + "name": "The _inbetween_ flexbox", + "href": "https://www.w3.org/TR/2011/WD-css3-flexbox-20111129/" + }], + "warnings": ["This represents an old syntax, not the latest standard syntax."] +} +!*/ +/*! +{ + "name": "Flex Gap", + "property": "flexgap", + "caniuse": "flexbox-gap", + "tags": ["css", "flexbox"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-align-3/#gaps" + }], + "authors": ["Chris Smith (@chris13524)"] +} +!*/ +/*! +{ + "name": "Flex Line Wrapping", + "property": "flexwrap", + "tags": ["css", "flexbox"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-flexbox-1/" + }], + "warnings": [ + "Does not imply a modern implementation – see documentation." + ] +} +!*/ +/*! +{ + "name": "CSS :focus-visible pseudo-selector", + "caniuse": "css-focus-visible", + "property": "focusvisible", + "authors": ["@esaborit4code"], + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS :focus-within pseudo-selector", + "caniuse": "css-focus-within", + "property": "focuswithin", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "Font Display", + "property": "fontdisplay", + "authors": ["Patrick Kettner"], + "caniuse": "css-font-rendering-controls", + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-fonts-4/#font-display-desc" + }, { + "name": "`font-display` for the masses", + "href": "https://css-tricks.com/font-display-masses/" + }] +} +!*/ +/*! +{ + "name": "@font-face", + "property": "fontface", + "authors": ["Diego Perini", "Mat Marquis"], + "tags": ["css"], + "knownBugs": [ + "False Positive: WebOS https://github.com/Modernizr/Modernizr/issues/342", + "False Positive: WP7 https://github.com/Modernizr/Modernizr/issues/538" + ], + "notes": [{ + "name": "@font-face detection routine by Diego Perini", + "href": "http://javascript.nwbox.com/CSSSupport/" + }, { + "name": "Filament Group @font-face compatibility research", + "href": "https://docs.google.com/presentation/d/1n4NyG4uPRjAA8zn_pSQ_Ket0RhcWC6QlZ6LMjKeECo0/edit#slide=id.p" + }, { + "name": "Filament Grunticon/@font-face device testing results", + "href": "https://docs.google.com/spreadsheet/ccc?key=0Ag5_yGvxpINRdHFYeUJPNnZMWUZKR2ItMEpRTXZPdUE#gid=0" + }, { + "name": "CSS fonts on Android", + "href": "https://stackoverflow.com/questions/3200069/css-fonts-on-android" + }, { + "name": "@font-face and Android", + "href": "http://archivist.incutio.com/viewlist/css-discuss/115960" + }] +} +!*/ +/*! +{ + "name": "CSS Generated Content", + "property": "generatedcontent", + "tags": ["css"], + "warnings": ["Android may not return correct height for anything below 7px in old versions #738"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-selectors/#gen-content" + }, { + "name": "MDN Docs on :before", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/::before" + }, { + "name": "MDN Docs on :after", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/::after" + }] +} +!*/ +/*! +{ + "name": "CSS Gradients", + "caniuse": "css-gradients", + "property": "cssgradients", + "tags": ["css"], + "knownBugs": ["False-positives on webOS (https://github.com/Modernizr/Modernizr/issues/202)"], + "notes": [{ + "name": "Webkit Gradient Syntax", + "href": "https://webkit.org/blog/175/introducing-css-gradients/" + }, { + "name": "Linear Gradient Syntax", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient" + }, { + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-images-3/#gradients" + }] +} +!*/ +/*! { + "name": "CSS Hairline", + "property": "hairline", + "tags": ["css"], + "authors": ["strarsis"], + "notes": [{ + "name": "Blog post about CSS retina hairlines", + "href": "http://dieulot.net/css-retina-hairline" + }, { + "name": "Derived from", + "href": "https://gist.github.com/dieulot/520a49463f6058fbc8d1" + }] +} +!*/ +/*! +{ + "name": "CSS HSLA Colors", + "caniuse": "css3-colors", + "property": "hsla", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Hyphens", + "caniuse": "css-hyphens", + "property": ["csshyphens", "softhyphens", "softhyphensfind"], + "tags": ["css"], + "builderAliases": ["css_hyphens"], + "async": true, + "authors": ["David Newton"], + "warnings": [ + "These tests currently require document.body to be present", + "If loading Hyphenator.js via yepnope, be cautious of issue 158: https://github.com/mnater/hyphenator/issues/158", + "This is very large – only include it if you absolutely need it" + ], + "notes": [{ + "name": "The Current State of Hyphenation on the Web.", + "href": "https://davidnewton.ca/the-current-state-of-hyphenation-on-the-web" + }, { + "name": "Hyphenation Test Page", + "href": "https://web.archive.org/web/20150319125549/http://davidnewton.ca/demos/hyphenation/test.html" + }, { + "name": "Hyphenation is Language Specific", + "href": "https://code.google.com/p/hyphenator/source/diff?spec=svn975&r=975&format=side&path=/trunk/Hyphenator.js#sc_svn975_313" + }, { + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/312" + }] +} +!*/ +/*! +{ + "name": "CSS :invalid pseudo-class", + "property": "cssinvalid", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:invalid" + }] +} +!*/ +/*! +{ + "name": "CSS :last-child pseudo-selector", + "caniuse": "css-sel3", + "property": "lastchild", + "tags": ["css"], + "builderAliases": ["css_lastchild"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/304" + }] +} +!*/ +/*! +{ + "name": "CSS Mask", + "caniuse": "css-masks", + "property": "cssmask", + "tags": ["css"], + "builderAliases": ["css_mask"], + "notes": [{ + "name": "Webkit blog on CSS Masks", + "href": "https://webkit.org/blog/181/css-masks/" + }, { + "name": "Safari Docs", + "href": "https://developer.apple.com/library/archive/documentation/InternetWeb/Conceptual/SafariVisualEffectsProgGuide/Masks/Masks.html" + }, { + "name": "CSS SVG mask", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/mask" + }, { + "name": "Combine with clippaths for awesomeness", + "href": "https://web.archive.org/web/20150508193041/http://generic.cx:80/for/webkit/test.html" + }] +} +!*/ +/*! +{ + "name": "CSS Media Queries", + "caniuse": "css-mediaqueries", + "property": "mediaqueries", + "tags": ["css"], + "builderAliases": ["css_mediaqueries"] +} +!*/ +/*! +{ + "name": "CSS Multiple Backgrounds", + "caniuse": "multibackgrounds", + "property": "multiplebgs", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS :nth-child pseudo-selector", + "caniuse": "css-sel3", + "property": "nthchild", + "tags": ["css"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/685" + }, { + "name": "Sitepoint :nth-child documentation", + "href": "https://www.sitepoint.com/atoz-css-screencast-nth-child/" + }], + "authors": ["@emilchristensen"], + "knownBugs": ["Known false negative in Safari 3.1 and Safari 3.2.2"] +} +!*/ +/*! +{ + "name": "CSS Object Fit", + "caniuse": "object-fit", + "property": "objectfit", + "tags": ["css"], + "builderAliases": ["css_objectfit"], + "notes": [{ + "name": "Opera Article on Object Fit", + "href": "https://dev.opera.com/articles/css3-object-fit-object-position/" + }] +} +!*/ +/*! +{ + "name": "CSS Opacity", + "caniuse": "css-opacity", + "property": "opacity", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Overflow Scrolling", + "property": "overflowscrolling", + "tags": ["css"], + "builderAliases": ["css_overflow_scrolling"], + "notes": [{ + "name": "Article on iOS overflow scrolling", + "href": "https://css-tricks.com/snippets/css/momentum-scrolling-on-ios-overflow-elements/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-overflow-scrolling" + }] +} +!*/ +/*! +{ + "name": "CSS Pointer Events", + "caniuse": "pointer-events", + "property": "csspointerevents", + "authors": ["ausi"], + "tags": ["css"], + "builderAliases": ["css_pointerevents"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events" + }, { + "name": "Test Project Page", + "href": "https://ausi.github.com/Feature-detection-technique-for-pointer-events/" + }, { + "name": "Test Project Wiki", + "href": "https://github.com/ausi/Feature-detection-technique-for-pointer-events/wiki" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/80" + }] +} +!*/ +/*! +{ + "name": "CSS position: sticky", + "property": "csspositionsticky", + "tags": ["css"], + "builderAliases": ["css_positionsticky"], + "notes": [{ + "name": "Chrome bug report", + "href": "https://bugs.chromium.org/p/chromium/issues/detail?id=322972" + }], + "warnings": ["using position:sticky on anything but top aligned elements is buggy in Chrome < 37 and iOS <=7+"] +} +!*/ +/*! +{ + "name": "CSS Generated Content Animations", + "property": "csspseudoanimations", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Transitions", + "property": "csstransitions", + "caniuse": "css-transitions", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Generated Content Transitions", + "property": "csspseudotransitions", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Reflections", + "caniuse": "css-reflections", + "property": "cssreflections", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Regions", + "caniuse": "css-regions", + "authors": ["Mihai Balan"], + "property": "regions", + "tags": ["css"], + "builderAliases": ["css_regions"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-regions/" + }] +} +!*/ +/*! +{ + "name": "CSS Font rem Units", + "caniuse": "rem", + "authors": ["nsfmc"], + "property": "cssremunit", + "tags": ["css"], + "builderAliases": ["css_remunit"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-values/#relative0" + }, { + "name": "Font Size with rem by Jonathan Snook", + "href": "https://snook.ca/archives/html_and_css/font-size-with-rem" + }] +} +!*/ +/*! +{ + "name": "CSS UI Resize", + "property": "cssresize", + "caniuse": "css-resize", + "tags": ["css"], + "builderAliases": ["css_resize"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-ui/#resize" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/CSS/resize" + }] +} +!*/ +/*! +{ + "name": "CSS rgba", + "caniuse": "css3-colors", + "property": "rgba", + "tags": ["css"], + "notes": [{ + "name": "CSSTricks Tutorial", + "href": "https://css-tricks.com/rgba-browser-support/" + }] +} +!*/ +/*! +{ + "name": "CSS Stylable Scrollbars", + "property": "cssscrollbar", + "tags": ["css"], + "builderAliases": ["css_scrollbars"] +} +!*/ +/*! +{ + "name": "Scroll Snap Points", + "property": "scrollsnappoints", + "caniuse": "css-snappoints", + "notes": [{ + "name": "Setting native-like scrolling offsets in CSS with Scrolling Snap Points", + "href": "http://generatedcontent.org/post/66817675443/setting-native-like-scrolling-offsets-in-css-with" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap_Points" + }], + "polyfills": ["scrollsnap"] +} +!*/ +/*! +{ + "name": "CSS Shapes", + "property": "shapes", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-shapes" + }, { + "name": "Examples from Adobe", + "href": "https://web.archive.org/web/20171230010236/http://webplatform.adobe.com:80/shapes" + }, { + "name": "Examples from CSS-Tricks", + "href": "https://css-tricks.com/examples/ShapesOfCSS/" + }] +} +!*/ +/*! +{ + "name": "CSS general sibling selector", + "caniuse": "css-sel3", + "property": "siblinggeneral", + "tags": ["css"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/pull/889" + }] +} +!*/ +/*! +{ + "name": "CSS Subpixel Fonts", + "property": "subpixelfont", + "tags": ["css"], + "builderAliases": ["css_subpixelfont"], + "authors": ["@derSchepp", "@gerritvanaaken", "@rodneyrehm", "@yatil", "@ryanseddon"], + "notes": [{ + "name": "Origin Test", + "href": "https://github.com/gerritvanaaken/subpixeldetect" + }] +} +!*/ +/*! +{ + "name": "CSS :target pseudo-class", + "caniuse": "css-sel3", + "property": "target", + "tags": ["css"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:target" + }], + "authors": ["@zachleat"], + "warnings": ["Opera Mini supports :target but doesn't update the hash for anchor links."] +} +!*/ +/*! +{ + "name": "CSS text-align-last", + "property": "textalignlast", + "caniuse": "css-text-align-last", + "tags": ["css"], + "warnings": ["IE does not support the 'start' or 'end' values."], + "notes": [{ + "name": "Quirksmode", + "href": "https://www.quirksmode.org/css/text/textalignlast.html" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/text-align-last" + }] +} +!*/ +/*! +{ + "name": "CSS textDecoration", + "property": "textdecoration", + "caniuse": "text-decoration", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css-text-decor-3/#line-decoration" + }] +} +!*/ +/*! +{ + "name": "CSS textshadow", + "property": "textshadow", + "caniuse": "css-textshadow", + "tags": ["css"], + "knownBugs": ["FF3.0 will false positive on this test"] +} +!*/ +/*! +{ + "name": "CSS Transforms", + "property": "csstransforms", + "caniuse": "transforms2d", + "tags": ["css"] +} +!*/ +/*! +{ + "name": "CSS Transforms 3D", + "property": "csstransforms3d", + "caniuse": "transforms3d", + "tags": ["css"], + "knownBugs": [ + "Chrome may occasionally fail this test on some systems; more info: https://bugs.chromium.org/p/chromium/issues/detail?id=129004, however, the issue has since been closed (marked as fixed)." + ] +} +!*/ +/*! +{ + "name": "CSS Transforms Level 2", + "property": "csstransformslevel2", + "authors": ["rupl"], + "tags": ["css"], + "notes": [{ + "name": "CSSWG Draft Spec", + "href": "https://drafts.csswg.org/css-transforms-2/" + }] +} +!*/ +/*! +{ + "name": "CSS Transform Style preserve-3d", + "property": "preserve3d", + "authors": ["denyskoch", "aFarkas"], + "tags": ["css"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/transform-style" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/1748" + }] +} +!*/ +/*! +{ + "name": "CSS user-select", + "property": "userselect", + "caniuse": "user-select-none", + "authors": ["ryan seddon"], + "tags": ["css"], + "builderAliases": ["css_userselect"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/250" + }] +} +!*/ +/*! +{ + "name": "CSS :valid pseudo-class", + "property": "cssvalid", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/CSS/:valid" + }] +} +!*/ +/*! +{ + "name": "Variable Open Type Fonts", + "property": "variablefonts", + "authors": ["Patrick Kettner"], + "tags": ["css"], + "notes": [{ + "name": "Variable fonts on the web", + "href": "https://webkit.org/blog/7051/variable-fonts-on-the-web/" + }, { + "name": "Variable fonts for responsive design", + "href": "https://alistapart.com/blog/post/variable-fonts-for-responsive-design" + }] +} +!*/ +/*! +{ + "name": "CSS vh unit", + "property": "cssvhunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vhunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "Similar JSFiddle", + "href": "https://jsfiddle.net/FWeinb/etnYC/" + }] +} +!*/ +/*! +{ + "name": "CSS vmax unit", + "property": "cssvmaxunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vmaxunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/glsee/JDsWQ/4/" + }] +} +!*/ +/*! +{ + "name": "CSS vmin unit", + "property": "cssvminunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vminunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/glsee/JRmdq/8/" + }] +} +!*/ +/*! +{ + "name": "CSS vw unit", + "property": "cssvwunit", + "caniuse": "viewport-units", + "tags": ["css"], + "builderAliases": ["css_vwunit"], + "notes": [{ + "name": "Related Modernizr Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/572" + }, { + "name": "JSFiddle Example", + "href": "https://jsfiddle.net/FWeinb/etnYC/" + }] +} +!*/ +/*! +{ + "name": "will-change", + "property": "willchange", + "caniuse": "will-change", + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/css-will-change/" + }] +} +!*/ +/*! +{ + "name": "CSS wrap-flow", + "property": "wrapflow", + "tags": ["css"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/css3-exclusions" + }, { + "name": "Example by Louie Rootfield", + "href": "https://webdesign.tutsplus.com/tutorials/css-exclusions--cms-28087" + }] +} +!*/ +/*! +{ + "name": "Custom Elements API", + "property": "customelements", + "caniuse": "custom-elementsv1", + "tags": ["customelements"], + "polyfills": ["customelements"], + "notes": [{ + "name": "Specs for Custom Elements", + "href": "https://www.w3.org/TR/custom-elements/" + }] +} +!*/ +/*! +{ + "name": "Custom protocol handler", + "property": "customprotocolhandler", + "authors": ["Ben Schwarz"], + "builderAliases": ["custom_protocol_handler"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/dev/system-state.html#custom-handlers" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/navigator.registerProtocolHandler" + }] +} +!*/ +/*! +{ + "name": "Dart", + "property": "dart", + "authors": ["Theodoor van Donge"], + "notes": [{ + "name": "Language website", + "href": "https://www.dartlang.org/" + }] +} +!*/ +/*! +{ + "name": "DataView", + "property": "dataview", + "authors": ["Addy Osmani"], + "builderAliases": ["dataview_api"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/JavaScript_typed_arrays/DataView" + }], + "polyfills": ["jdataview"] +} +!*/ +/*! +{ + "name": "classList", + "caniuse": "classlist", + "property": "classlist", + "tags": ["dom"], + "builderAliases": ["dataview_api"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/DOM/element.classList" + }] +} +!*/ +/*! +{ + "name": "createElement with Attributes", + "property": ["createelementattrs", "createelement-attrs"], + "tags": ["dom"], + "builderAliases": ["dom_createElement_attrs"], + "authors": ["James A. Rosen"], + "notes": [{ + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/258" + }] +} +!*/ +/*! +{ + "name": "dataset API", + "caniuse": "dataset", + "property": "dataset", + "tags": ["dom"], + "builderAliases": ["dom_dataset"], + "authors": ["@phiggins42"] +} +!*/ +/*! +{ + "name": "Document Fragment", + "property": "documentfragment", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/REC-DOM-Level-1/level-one-core.html#ID-B63ED1A3" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment" + }, { + "name": "QuirksMode Compatibility Tables", + "href": "https://www.quirksmode.org/m/w3c_core.html#t112" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "knownBugs": ["false-positive on Blackberry 9500, see QuirksMode note"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "[hidden] Attribute", + "property": "hidden", + "tags": ["dom"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/dev/interaction.html#the-hidden-attribute" + }, { + "name": "original implementation of detect code", + "href": "https://github.com/aFarkas/html5shiv/blob/bf4fcc4/src/html5shiv.js#L38" + }], + "polyfills": ["html5shiv"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Intersection Observer", + "property": "intersectionobserver", + "caniuse": "intersectionobserver", + "tags": ["dom"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/IntersectionObserver/" + }, { + "name": "IntersectionObserver polyfill", + "href": "https://github.com/w3c/IntersectionObserver/tree/master/polyfill" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/docs/Web/API/Intersection_Observer_API" + }] +} +!*/ +/*! +{ + "name": "microdata", + "property": "microdata", + "tags": ["dom"], + "builderAliases": ["dom_microdata"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/microdata/" + }] +} +!*/ +/*! +{ + "name": "DOM4 MutationObserver", + "property": "mutationobserver", + "caniuse": "mutationobserver", + "tags": ["dom"], + "authors": ["Karel Sedláček (@ksdlck)"], + "polyfills": ["mutationobservers"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver" + }] +} +!*/ +/*! +{ + "property": "passiveeventlisteners", + "caniuse": "passive-event-listener", + "tags": ["dom"], + "authors": ["Rick Byers"], + "name": "Passive event listeners", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://dom.spec.whatwg.org/#dom-addeventlisteneroptions-passive" + }, { + "name": "WICG explainer", + "href": "https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md" + }] +} +!*/ +/*! +{ + "name": "Shadow DOM API", + "property": "shadowroot", + "caniuse": "shadowdomv1", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot" + }], + "authors": ["Kevin Coyle (@kevin-coyle-unipro)", "Pascal Lim (@pascalim)"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "Shadow DOM API (Legacy)", + "property": "shadowrootlegacy", + "caniuse": "shadowdom", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Element/createShadowRoot" + }], + "authors": ["Kevin Coyle (@kevin-coyle-unipro)", "Pascal Lim (@pascalim)"], + "tags": ["dom"] +} +!*/ +/*! +{ + "name": "bdi Element", + "property": "bdi", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/bdi" + }] +} +!*/ +/*! +{ + "name": "details Element", + "caniuse": "details", + "property": "details", + "tags": ["elem"], + "builderAliases": ["elem_details"], + "authors": ["@mathias"], + "notes": [{ + "name": "Mathias' Original", + "href": "https://mathiasbynens.be/notes/html5-details-jquery#comment-35" + }] +} +!*/ +/*! +{ + "name": "output Element", + "property": "outputelem", + "tags": ["elem"], + "builderAliases": ["elem_output"], + "notes": [{ + "name": "WhatWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-elements.html#the-output-element" + }] +} +!*/ +/*! +{ + "name": "picture Element", + "property": "picture", + "tags": ["elem"], + "authors": ["Scott Jehl", "Mat Marquis"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#embedded-content" + }, { + "name": "Relevant spec issue", + "href": "https://github.com/ResponsiveImagesCG/picture-element/issues/87" + }] +} +!*/ +/*! +{ + "name": "progress Element", + "caniuse": "progress", + "property": ["progressbar", "meter"], + "tags": ["elem"], + "builderAliases": ["elem_progress_meter"], + "authors": ["Stefan Wallin"] +} +!*/ +/*! +{ + "name": "ruby, rp, rt Elements", + "caniuse": "ruby", + "property": "ruby", + "tags": ["elem"], + "builderAliases": ["elem_ruby"], + "authors": ["Cătălin Mariș"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-ruby-element" + }] +} +!*/ +/*! +{ + "name": "Template Tag", + "property": "template", + "caniuse": "template", + "tags": ["elem"], + "notes": [{ + "name": "HTML5Rocks Article", + "href": "https://www.html5rocks.com/en/tutorials/webcomponents/template/" + }, { + "name": "W3C Spec", + "href": "https://web.archive.org/web/20171130222649/http://www.w3.org/TR/html5/scripting-1.html" + }] +} +!*/ +/*! +{ + "name": "time Element", + "property": "time", + "tags": ["elem"], + "builderAliases": ["elem_time"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-time-element" + }] +} +!*/ +/*! +{ + "name": "Track element and Timed Text Track", + "property": ["texttrackapi", "track"], + "tags": ["elem"], + "builderAliases": ["elem_track"], + "authors": ["Addy Osmani"], + "notes": [{ + "name": "W3C Spec (Track Element)", + "href": "https://web.archive.org/web/20121119095019/http://www.w3.org/TR/html5/the-track-element.html#the-track-element" + }, { + "name": "W3C Spec (Track API)", + "href": "https://web.archive.org/web/20121119094620/http://www.w3.org/TR/html5/media-elements.html#text-track-api" + }], + "warnings": ["While IE10 has implemented the track element, IE10 does not expose the underlying APIs to create timed text tracks by JS (really sad)"] +} +!*/ +/*! +{ + "name": "Unknown Elements", + "property": "unknownelements", + "tags": ["elem"], + "notes": [{ + "name": "The Story of the HTML5 Shiv", + "href": "https://www.paulirish.com/2011/the-history-of-the-html5-shiv/" + }, { + "name": "original implementation of detect code", + "href": "https://github.com/aFarkas/html5shiv/blob/bf4fcc4/src/html5shiv.js#L36" + }], + "polyfills": ["html5shiv"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Emoji", + "property": "emoji" +} +!*/ +/*! +{ + "name": "ES5 Array", + "property": "es5array", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Date", + "property": "es5date", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Function", + "property": "es5function", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Object", + "property": "es5object", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim", "es5sham"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Strict Mode", + "property": "strictmode", + "caniuse": "use-strict", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "authors": ["@kangax"], + "tags": ["es5"], + "builderAliases": ["es5_strictmode"] +} +!*/ +/*! +{ + "name": "ES5 String", + "property": "es5string", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "JSON", + "property": "json", + "caniuse": "json", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Glossary/JSON" + }], + "polyfills": ["json2"] +} +!*/ +/*! +{ + "name": "ES5 Syntax", + "property": "es5syntax", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }, { + "name": "original implementation of detect code", + "href": "https://kangax.github.io/compat-table/es5/" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "warnings": ["This detect uses `eval()`, so CSP may be a problem."], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5 Immutable Undefined", + "property": "es5undefined", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }, { + "name": "original implementation of detect code", + "href": "https://kangax.github.io/compat-table/es5/" + }], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES5", + "property": "es5", + "caniuse": "es5", + "notes": [{ + "name": "ECMAScript 5.1 Language Specification", + "href": "https://www.ecma-international.org/ecma-262/5.1/" + }], + "polyfills": ["es5shim", "es5sham"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es5"] +} +!*/ +/*! +{ + "name": "ES6 Array", + "property": "es6array", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Arrow Functions", + "property": "arrow", + "authors": ["Vincent Riemer"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Class", + "property": "es6class", + "notes": [{ + "name": "ECMAScript 6 language specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions" + }], + "caniuse": "es6-class", + "authors": ["dabretin"], + "tags": ["es6"], + "builderAliases": ["class"] +} +!*/ +/*! +{ + "name": "ES6 Collections", + "property": "es6collections", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim", "weakmap"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Generators", + "property": "generators", + "authors": ["Michael Kachanovskyi"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Math", + "property": "es6math", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Number", + "property": "es6number", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Object", + "property": "es6object", + "notes": [{ + "name": "ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Promises", + "property": "promises", + "caniuse": "promises", + "polyfills": ["es6promises"], + "authors": ["Krister Kari", "Jake Archibald"], + "tags": ["es6"], + "notes": [{ + "name": "The ES6 promises spec", + "href": "https://github.com/domenic/promises-unwrapping" + }, { + "name": "Chromium dashboard - ES6 Promises", + "href": "https://www.chromestatus.com/features/5681726336532480" + }, { + "name": "JavaScript Promises: an Introduction", + "href": "https://developers.google.com/web/fundamentals/primers/promises/" + }] +} +!*/ +/*! +{ + "name": "ES6 Rest parameters", + "property": "restparameters", + "notes": [{ + "name": "ECMAScript 6 language specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-function-definitions" + }], + "caniuse": "rest", + "authors": ["dabretin"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Spread array", + "property": "spreadarray", + "notes": [{ + "name": "ECMAScript Specification", + "href": "https://tc39.es/ecma262/#sec-array-initializer" + }, + { + "name": "Article", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax" + }], + "caniuse": "mdn-javascript_operators_spread_spread_in_arrays", + "authors": ["dabretin"], + "warnings": ["not for object literals (implemented in ES7)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Template Strings", + "property": "stringtemplate", + "caniuse": "template-literals", + "builderAliases": ["templatestrings"], + "notes": [{ + "name": "ECMAScript 6 draft specification", + "href": "https://tc39wiki.calculist.org/es6/template-strings/" + }], + "authors": ["dabretin"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 String", + "property": "es6string", + "notes": [{ + "name": "ECMAScript 6 Specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/index.html" + }, { + "name": "Last ECMAScript Specification", + "href": "https://www.ecma-international.org/ecma-262/index.html" + }], + "polyfills": ["es6shim"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["es6"] +} +!*/ +/*! +{ + "name": "ES6 Symbol", + "property": "es6symbol", + "caniuse": "mdn-javascript_builtins_symbol", + "notes": [{ + "name": "Official ECMAScript 6 specification", + "href": "https://www.ecma-international.org/ecma-262/6.0/#sec-symbol-constructor" + },{ + "name": "MDN web docs", + "href": "https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Symbol" + }], + "polyfills": ["es6symbol"], + "authors": ["buhichan (@buhichan)"], + "tags": ["es6","symbol"] +} +!*/ +/*! +{ + "name": "ES7 Array", + "property": "es7array", + "notes": [{ + "name": "ECMAScript array Specification", + "href": "https://tc39.es/ecma262/#sec-array.prototype.includes" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES7 Rest destructuring", + "property": ["restdestructuringarray", "restdestructuringobject"], + "caniuse" : "destructuring%20assignment", + "notes": [{ + "name": "ECMAScript Destructuring Assignment Specification", + "href": "https://tc39.es/ecma262/#sec-destructuring-assignment" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES7 Spread object", + "property": "spreadobject", + "notes": [{ + "name": "ECMAScript array Specification", + "href": "http://www.ecma-international.org/ecma-262/#sec-object-initializer" + }], + "authors": ["dabretin"], + "tags": ["es7"] +} +!*/ +/*! +{ + "name": "ES8 Object", + "property": "es8object", + "notes": [{ + "name": "ECMAScript specification: Object.entries", + "href": "https://www.ecma-international.org/ecma-262/#sec-object.entries" + }, { + "name": "ECMAScript specification: Object.values", + "href": "https://www.ecma-international.org/ecma-262/#sec-object.values" + }], + "caniuse": "object-entries,object-values", + "authors": ["dabretin"], + "tags": ["es8"] +} +!*/ +/*! +{ + "name": "CustomEvent", + "property": "customevent", + "tags": ["customevent"], + "authors": ["Alberto Elias"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/DOM-Level-3-Events/#interface-CustomEvent" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/docs/Web/API/CustomEvent" + }], + "polyfills": ["eventlistener"] +} +!*/ +/*! +{ + "name": "Orientation and Motion Events", + "property": ["devicemotion", "deviceorientation"], + "caniuse": "deviceorientation", + "notes": [{ + "name": "W3C Editor's Draft Spec", + "href": "https://w3c.github.io/deviceorientation/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Detecting_device_orientation" + }], + "authors": ["Shi Chuan"], + "tags": ["event"], + "builderAliases": ["event_deviceorientation_motion"] +} +!*/ +/*! +{ + "name": "Event Listener", + "property": "eventlistener", + "caniuse": "addeventlistener", + "authors": ["Andrew Betts (@triblondon)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-Registration-interfaces" + }], + "polyfills": ["eventlistener"] +} +!*/ +/*! +{ + "name": "Force Touch Events", + "property": "forcetouch", + "authors": ["Kraig Walker"], + "notes": [{ + "name": "Responding to Force Touch Events from JavaScript", + "href": "https://developer.apple.com/library/archive/documentation/AppleApplications/Conceptual/SafariJSProgTopics/RespondingtoForceTouchEventsfromJavaScript.html" + }] +} +!*/ +/*! +{ + "name": "Hashchange event", + "property": "hashchange", + "caniuse": "hashchange", + "tags": ["history"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onhashchange" + }], + "polyfills": [ + "jquery-hashchange", + "moo-historymanager", + "jquery-ajaxy", + "hasher", + "shistory" + ] +} +!*/ +/*! +{ + "name": "onInput Event", + "property": "oninput", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers.oninput" + }, { + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/input.html#common-input-element-attributes" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/210" + }], + "authors": ["Patrick Kettner"], + "tags": ["event"] +} +!*/ +/*! +{ + "name": "DOM Pointer Events API", + "property": "pointerevents", + "caniuse": "pointer", + "tags": ["input"], + "authors": ["Stu Cox"], + "notes": [{ + "name": "W3C Spec (Pointer Events)", + "href": "https://www.w3.org/TR/pointerevents/" + }, { + "name": "W3C Spec (Pointer Events Level 2)", + "href": "https://www.w3.org/TR/pointerevents2/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent" + }], + "warnings": ["This property name now refers to W3C DOM PointerEvents: https://github.com/Modernizr/Modernizr/issues/548#issuecomment-12812099"], + "polyfills": ["pep"] +} +!*/ +/*! +{ + "name": "Proximity API", + "property": "proximity", + "authors": ["Cătălin Mariș"], + "tags": ["events", "proximity"], + "caniuse": "proximity", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Proximity_Events" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/proximity/" + }] +} +!*/ +/*! +{ + "name": "File API", + "property": "filereader", + "caniuse": "fileapi", + "notes": [{ + "name": "W3C Working Draft Spec", + "href": "https://www.w3.org/TR/FileAPI/" + }], + "tags": ["file"], + "builderAliases": ["file_api"], + "knownBugs": ["Will fail in Safari 5 due to its lack of support for the standards defined FileReader object"] +} +!*/ +/*! +{ + "name": "Filesystem API", + "property": "filesystem", + "caniuse": "filesystem", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/file-system-api/" + }], + "authors": ["Eric Bidelman (@ebidel)"], + "tags": ["file"], + "builderAliases": ["file_filesystem"], + "knownBugs": ["The API will be present in Chrome incognito, but will throw an exception. See crbug.com/93417"] +} +!*/ +/*! +{ + "name": "Flash", + "property": "flash", + "tags": ["flash"], + "polyfills": ["shumway"] +} +!*/ +/*! +{ + "name": "Fullscreen API", + "property": "fullscreen", + "caniuse": "fullscreen", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/API/Fullscreen" + }], + "polyfills": ["screenfulljs"], + "builderAliases": ["fullscreen_api"] +} +!*/ +/*! +{ + "name": "GamePad API", + "property": "gamepads", + "caniuse": "gamepad", + "authors": ["Eric Bidelman"], + "tags": ["media"], + "warnings": ["In new browsers it may return false in non-HTTPS connections"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/gamepad/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/doodles/gamepad/#toc-featuredetect" + }] +} +!*/ +/*! +{ + "name": "Geolocation API", + "property": "geolocation", + "caniuse": "geolocation", + "tags": ["media"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/WebAPI/Using_geolocation" + }], + "polyfills": [ + "joshuabell-polyfill", + "webshims", + "geo-location-javascript", + "geolocation-api-polyfill" + ] +} +!*/ +/*! +{ + "name": "Hidden Scrollbar", + "property": "hiddenscroll", + "authors": ["Oleg Korsunsky"], + "tags": ["overlay"], + "notes": [{ + "name": "Overlay Scrollbar description", + "href": "https://developer.apple.com/library/mac/releasenotes/MacOSX/WhatsNewInOSX/Articles/MacOSX10_7.html#//apple_ref/doc/uid/TP40010355-SW39" + }, { + "name": "Video example of overlay scrollbars", + "href": "https://gfycat.com/FoolishMeaslyAtlanticsharpnosepuffer" + }] +} +!*/ +/*! +{ + "name": "History API", + "property": "history", + "caniuse": "history", + "tags": ["history"], + "authors": ["Hay Kranen", "Alexander Farkas"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/html51/browsers.html#the-history-interface" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/window.history" + }], + "polyfills": ["historyjs", "html5historyapi"] +} +!*/ +/*! +{ + "name": "HTML Imports", + "property": "htmlimports", + "tags": ["html", "import"], + "polyfills": ["polymer-htmlimports"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/webcomponents/spec/imports/" + }, { + "name": "HTML Imports - #include for the web", + "href": "https://www.html5rocks.com/en/tutorials/webcomponents/imports/" + }] +} +!*/ +/*! +{ + "name": "IE8 compat mode", + "property": "ie8compat", + "authors": ["Erich Ocean"] +} +!*/ +/*! +{ + "name": "iframe[sandbox] Attribute", + "property": "sandbox", + "caniuse": "iframe-sandbox", + "tags": ["iframe"], + "builderAliases": ["iframe_sandbox"], + "notes": [ + { + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-sandbox" + }], + "knownBugs": ["False-positive on Firefox < 29"] +} +!*/ +/*! +{ + "name": "iframe[seamless] Attribute", + "property": "seamless", + "tags": ["iframe"], + "builderAliases": ["iframe_seamless"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-seamless" + }] +} +!*/ +/*! +{ + "name": "iframe[srcdoc] Attribute", + "property": "srcdoc", + "caniuse": "iframe-srcdoc", + "tags": ["iframe"], + "builderAliases": ["iframe_srcdoc"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#attr-iframe-srcdoc" + }] +} +!*/ +/*! +{ + "name": "Animated PNG", + "async": true, + "property": "apng", + "caniuse": "apng", + "tags": ["image"], + "builderAliases": ["img_apng"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/APNG" + }] +} +!*/ +/*! +{ + "name": "AVIF", + "async": true, + "property": "avif", + "caniuse": "avif", + "tags": ["image"], + "authors": ["Markel Ferro (@MarkelFe)"], + "polyfills": ["avifjs"], + "notes": [{ + "name": "Avif Spec", + "href": "https://aomediacodec.github.io/av1-avif/" + }] +} +!*/ +/*! +{ + "name": "Image crossOrigin", + "property": "imgcrossorigin", + "tags": ["image"], + "notes": [{ + "name": "Cross Domain Images and the Tainted Canvas", + "href": "https://blog.codepen.io/2013/10/08/cross-domain-images-tainted-canvas/" + }] +} +!*/ +/*! +{ + "name": "EXIF Orientation", + "property": "exiforientation", + "tags": ["image"], + "builderAliases": ["exif_orientation"], + "async": true, + "authors": ["Paul Sayre"], + "notes": [{ + "name": "Article by Dave Perrett", + "href": "https://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/" + }, { + "name": "Article by Calvin Hass", + "href": "https://www.impulseadventure.com/photo/exif-orientation.html" + }] +} +!*/ +/*! +{ + "name": "JPEG 2000", + "async": true, + "aliases": ["jpeg-2000", "jpg2"], + "property": "jpeg2000", + "caniuse": "jpeg2000", + "tags": ["image"], + "authors": ["@eric_wvgg"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/JPEG_2000" + }] +} +!*/ +/*! +{ + "name": "JPEG XR (extended range)", + "async": true, + "aliases": ["jpeg-xr"], + "property": "jpegxr", + "tags": ["image"], + "notes": [{ + "name": "Wikipedia Article", + "href": "https://en.wikipedia.org/wiki/JPEG_XR" + }] +} +!*/ +/*! +{ + "name": "image and iframe native lazy loading", + "property": "lazyloading", + "caniuse": "loading-lazy-attr", + "tags": ["image", "lazy", "loading"], + "notes": [{ + "name": "Native image lazy-loading for the web", + "href": "https://addyosmani.com/blog/lazy-loading/" + }] +} +!*/ +/*! +{ + "name": "sizes attribute", + "async": true, + "property": "sizes", + "tags": ["image"], + "authors": ["Mat Marquis"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element" + }, { + "name": "Srcset and sizes", + "href": "https://ericportis.com/posts/2014/srcset-sizes/" + }] +} +!*/ +/*! +{ + "name": "srcset attribute", + "property": "srcset", + "caniuse": "srcset", + "tags": ["image"], + "notes": [{ + "name": "Smashing Magazine Article", + "href": "https://www.smashingmagazine.com/2013/08/webkit-implements-srcset-and-why-its-a-good-thing/" + }, { + "name": "Generate multi-resolution images for srcset with Grunt", + "href": "https://addyosmani.com/blog/generate-multi-resolution-images-for-srcset-with-grunt/" + }] +} +!*/ +/*! +{ + "name": "Webp Alpha", + "async": true, + "property": "webpalpha", + "aliases": ["webp-alpha"], + "tags": ["image"], + "authors": ["Krister Kari", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "WebP Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Article about WebP support", + "href": "https://optimus.keycdn.com/support/webp-support/" + }, { + "name": "Chromium WebP announcement", + "href": "https://blog.chromium.org/2011/11/lossless-and-transparency-encoding-in.html?m=1" + }] +} +!*/ +/*! +{ + "name": "Webp Animation", + "async": true, + "property": "webpanimation", + "aliases": ["webp-animation"], + "tags": ["image"], + "authors": ["Krister Kari", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "WebP Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Chromium blog - Chrome 32 Beta: Animated WebP images and faster Chrome for Android touch input", + "href": "https://blog.chromium.org/2013/11/chrome-32-beta-animated-webp-images-and.html" + }] +} +!*/ +/*! +{ + "name": "Webp Lossless", + "async": true, + "property": ["webplossless", "webp-lossless"], + "tags": ["image"], + "authors": ["@amandeep", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "Webp Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Webp Lossless Spec", + "href": "https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification" + }] +} +!*/ +/*! +{ + "name": "Webp", + "async": true, + "property": "webp", + "caniuse": "webp", + "tags": ["image"], + "builderAliases": ["img_webp"], + "authors": ["Krister Kari", "@amandeep", "Rich Bradshaw", "Ryan Seddon", "Paul Irish"], + "notes": [{ + "name": "Webp Info", + "href": "https://developers.google.com/speed/webp/" + }, { + "name": "Chromium blog - Chrome 32 Beta: Animated WebP images and faster Chrome for Android touch input", + "href": "https://blog.chromium.org/2013/11/chrome-32-beta-animated-webp-images-and.html" + }, { + "name": "Webp Lossless Spec", + "href": "https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification" + }, { + "name": "Article about WebP support", + "href": "https://optimus.keycdn.com/support/webp-support/" + }, { + "name": "Chromium WebP announcement", + "href": "https://blog.chromium.org/2011/11/lossless-and-transparency-encoding-in.html?m=1" + }] +} +!*/ +/*! +{ + "name": "input[capture] Attribute", + "property": "capture", + "tags": ["video", "image", "audio", "media", "attribute"], + "notes": [{ + "name": "W3C Draft Spec", + "href": "https://www.w3.org/TR/html-media-capture/" + }] +} +!*/ +/*! +{ + "name": "input[file] Attribute", + "property": "fileinput", + "caniuse": "forms", + "tags": ["file", "forms", "input"], + "builderAliases": ["forms_fileinput"] +} +!*/ +/*! +{ + "name": "input[directory] Attribute", + "property": "directory", + "authors": ["silverwind"], + "tags": ["file", "input", "attribute"] +} +!*/ +/*! +{ + "name": "input formaction", + "property": "inputformaction", + "aliases": ["input-formaction"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formaction" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formaction-attribute/" + }], + "polyfills": ["webshims"] +} +!*/ +/*! +{ + "name": "input[form] Attribute", + "property": "formattribute", + "tags": ["attribute", "forms", "input"], + "builderAliases": ["forms_formattribute"] +} +!*/ +/*! +{ + "name": "input formenctype", + "property": "inputformenctype", + "aliases": ["input-formenctype"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formenctype" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formenctype-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "input formmethod", + "property": "inputformmethod", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formmethod" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formmethod-attribute/" + }], + "polyfills": ["webshims"] +} +!*/ +/*! +{ + "name": "input formnovalidate", + "property": "inputformnovalidate", + "aliases": ["input-formnovalidate"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formnovalidate" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formnovalidate-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "input formtarget", + "property": "inputformtarget", + "aliases": ["input-formtarget"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fs-formtarget" + }, { + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/formtarget-attribute/" + }], + "polyfills": ["html5formshim"] +} +!*/ +/*! +{ + "name": "Input attributes", + "property": "input", + "tags": ["forms"], + "authors": ["Mike Taylor"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/input.html#input-type-attr-summary" + }], + "knownBugs": ["Some blackberry devices report false positive for input.multiple"] +} +!*/ +/*! +{ + "name": "Form input types", + "property": "inputtypes", + "caniuse": "forms", + "tags": ["forms"], + "authors": ["Mike Taylor"], + "polyfills": [ + "jquerytools", + "webshims", + "h5f", + "webforms2", + "nwxforms", + "fdslider", + "html5slider", + "galleryhtml5forms", + "jscolor", + "html5formshim", + "selectedoptionsjs", + "formvalidationjs" + ] +} +!*/ +/*! +{ + "name": "Form Validation", + "property": "formvalidation", + "tags": ["forms", "validation", "attribute"], + "builderAliases": ["forms_validation"] +} +!*/ +/*! +{ + "name": "input[type=\"number\"] Localization", + "property": "localizednumber", + "tags": ["forms", "localization", "attribute"], + "authors": ["Peter Janes"], + "notes": [{ + "name": "Webkit Bug Tracker Listing", + "href": "https://bugs.webkit.org/show_bug.cgi?id=42484" + }, { + "name": "Based on This", + "href": "https://trac.webkit.org/browser/trunk/LayoutTests/fast/forms/script-tests/input-number-keyoperation.js?rev=80096#L9" + }], + "knownBugs": ["Only ever returns true if the browser/OS is configured to use comma as a decimal separator. This is probably fine for most use cases."] +} +!*/ +/*! +{ + "name": "input[search] search event", + "property": "inputsearchevent", + "tags": ["input","search"], + "authors": ["Calvin Webster"], + "notes": [{ + "name": "Wufoo demo", + "href": "https://www.wufoo.com/html5/search-type/" + }, { + "name": "CSS Tricks", + "href": "https://css-tricks.com/webkit-html5-search-inputs/" + }] +} +!*/ +/*! +{ + "name": "placeholder attribute", + "property": "placeholder", + "tags": ["forms", "attribute"], + "builderAliases": ["forms_placeholder"] +} +!*/ +/*! +{ + "name": "form#requestAutocomplete()", + "property": "requestautocomplete", + "tags": ["form", "forms", "requestAutocomplete", "payments"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://wiki.whatwg.org/wiki/RequestAutocomplete" + }] +} +!*/ +/*! +{ + "name": "Internationalization API", + "property": "intl", + "caniuse": "internationalization", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl" + }, { + "name": "ECMAScript spec", + "href": "https://www.ecma-international.org/ecma-402/1.0/" + }] +} + !*/ +/*! +{ + "name": "Font Ligatures", + "property": "ligatures", + "caniuse": "font-feature", + "notes": [{ + "name": "Cross-browser Web Fonts", + "href": "https://www.sitepoint.com/cross-browser-web-fonts-part-3/" + }] +} +!*/ +/*! +{ + "name": "Reverse Ordered Lists", + "property": "olreversed", + "notes": [{ + "name": "Impressive Webs article", + "href": "https://www.impressivewebs.com/reverse-ordered-lists-html5/" + }], + "builderAliases": ["lists_reversed"] +} +!*/ +/*! +{ + "name": "MathML", + "property": "mathml", + "caniuse": "mathml", + "authors": ["Addy Osmani", "Davide P. Cervone", "David Carlisle"], + "knownBugs": ["Firefox < 4 will likely return a false, however it does support MathML inside XHTML documents"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/Math/" + }], + "polyfills": ["mathjax"] +} +!*/ +/*! +{ + "name": "Media Source Extensions API", + "caniuse": "mediasource", + "property": "mediasource", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API" + }], + "builderAliases": ["media_source_extension_api"] +} +!*/ +/*! +{ + "name": "Hover Media Query", + "property": "hovermq", + "tags": ["mediaquery"] +} +!*/ +/*! +{ + "name": "Pointer Media Query", + "property": "pointermq", + "tags": ["mediaquery"] +} +!*/ +/*! +{ + "name": "Message Channel", + "property": "messagechannel", + "authors": ["Raju Konga (@kongaraju)"], + "caniuse": "channel-messaging", + "tags": ["performance", "messagechannel"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2011/WD-webmessaging-20110317/#message-channels" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API/Using_channel_messaging" + }] +} +!*/ +/*! +{ + "name": "Beacon API", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/navigator.sendBeacon" + }, { + "name": "W3C Spec", + "href": "https://w3c.github.io/beacon/" + }], + "property": "beacon", + "caniuse": "beacon", + "tags": ["beacon", "network"], + "authors": ["Cătălin Mariș"] +} +!*/ +/*! +{ + "name": "Connection Effective Type", + "notes": [{ + "name": "MDN documentation", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType" + }], + "property": "connectioneffectivetype", + "builderAliases": ["network_connection"], + "tags": ["network"] +} +!*/ +/*! +{ + "name": "Low Bandwidth Connection", + "property": "lowbandwidth", + "tags": ["network"], + "builderAliases": ["network_connection"] +} +!*/ +/*! +{ + "name": "Server Sent Events", + "property": "eventsource", + "tags": ["network"], + "builderAliases": ["network_eventsource"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events" + }] +} +!*/ +/*! +{ + "name": "Fetch API", + "property": "fetch", + "tags": ["network"], + "caniuse": "fetch", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://fetch.spec.whatwg.org/" + }], + "polyfills": ["fetch"] +} +!*/ +/*! +{ + "name": "XHR responseType='arraybuffer'", + "property": "xhrresponsetypearraybuffer", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='blob'", + "property": "xhrresponsetypeblob", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='document'", + "property": "xhrresponsetypedocument", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='json'", + "property": "xhrresponsetypejson", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }, { + "name": "Explanation of xhr.responseType='json'", + "href": "https://mathiasbynens.be/notes/xhr-responsetype-json" + }] +} +!*/ +/*! +{ + "name": "XHR responseType='text'", + "property": "xhrresponsetypetext", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XHR responseType", + "property": "xhrresponsetype", + "tags": ["network"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://xhr.spec.whatwg.org/#the-responsetype-attribute" + }] +} +!*/ +/*! +{ + "name": "XML HTTP Request Level 2 XHR2", + "property": "xhr2", + "caniuse": "xhr2", + "tags": ["network"], + "builderAliases": ["network_xhr2"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/XMLHttpRequest2/" + }, { + "name": "Details on Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/385" + }] +} +!*/ +/*! +{ + "name": "Notification", + "property": "notification", + "caniuse": "notifications", + "authors": ["Theodoor van Donge", "Hendrik Beskow"], + "notes": [{ + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/notifications/quick/" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/notifications/" + }, { + "name": "Changes in Chrome to Notifications API due to Service Worker Push Notifications", + "href": "https://developers.google.com/web/updates/2015/05/Notifying-you-of-notificiation-changes" + }], + "knownBugs": ["Possibility of false-positive on Chrome for Android if permissions we're granted for a website prior to Chrome 44."], + "polyfills": ["desktop-notify", "html5-notifications"] +} +!*/ +/*! +{ + "name": "Page Visibility API", + "property": "pagevisibility", + "caniuse": "pagevisibility", + "tags": ["performance"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/DOM/Using_the_Page_Visibility_API" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2011/WD-page-visibility-20110602/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/pagevisibility/intro/" + }], + "polyfills": ["visibilityjs", "visiblyjs", "jquery-visibility"] +} +!*/ +/*! +{ + "name": "Navigation Timing API", + "property": "performance", + "caniuse": "nav-timing", + "tags": ["performance"], + "authors": ["Scott Murphy (@uxder)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/navigation-timing/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/webperformance/basics/" + }], + "polyfills": ["perfnow"] +} +!*/ +/*! +{ + "name": "Pointer Lock API", + "property": "pointerlock", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/API/Pointer_Lock_API" + }], + "builderAliases": ["pointerlock_api"] +} +!*/ +/*! +{ + "name": "postMessage", + "property": "postmessage", + "caniuse": "x-doc-messaging", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/webmessaging/#crossDocumentMessages" + }], + "polyfills": ["easyxdm", "postmessage-jquery"], + "knownBugs": [ + "structuredclones - Android 2&3 can not send a structured clone of dates, filelists or regexps.", + "Some old WebKit versions have bugs." + ], + "warnings": ["To be safe you should stick with object, array, number and pixeldata."] +} +!*/ +/*! +{ + "name": "Proxy Object", + "property": "proxy", + "caniuse": "proxy", + "authors": ["Brock Beaudry"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" + }], + "polyfills": [ + "harmony-reflect" + ] +} +!*/ +/*! +{ + "name": "QuerySelector", + "property": "queryselector", + "caniuse": "queryselector", + "tags": ["queryselector"], + "authors": ["Andrew Betts (@triblondon)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/selectors-api/#queryselectorall" + }], + "polyfills": ["css-selector-engine"] +} +!*/ +/*! +{ + "name": "rel=prefetch", + "property": "prefetch", + "caniuse": "link-rel-prefetch", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/resource-hints/#prefetch" + }, { + "name": "Related Github Issue", + "href": "https://github.com/Modernizr/Modernizr/issues/2536" + }] +} +!*/ +/*! +{ + "name": "requestAnimationFrame", + "property": "requestanimationframe", + "aliases": ["raf"], + "caniuse": "requestanimationframe", + "tags": ["animation"], + "authors": ["Addy Osmani"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/animation-timing/" + }], + "polyfills": ["raf"] +} +!*/ +/*! +{ + "name": "script[async]", + "property": "scriptasync", + "caniuse": "script-async", + "tags": ["script"], + "builderAliases": ["script_async"], + "authors": ["Theodoor van Donge"] +} +!*/ +/*! +{ + "name": "script[defer]", + "property": "scriptdefer", + "caniuse": "script-defer", + "tags": ["script"], + "builderAliases": ["script_defer"], + "authors": ["Theodoor van Donge"], + "warnings": ["Browser implementation of the `defer` attribute vary: https://stackoverflow.com/questions/3952009/defer-attribute-chrome#answer-3982619"], + "knownBugs": ["False positive in Opera 12"] +} +!*/ +/*! +{ + "name": "scrollToOptions dictionary", + "property": "scrolltooptions", + "caniuse": "mdn-api_scrolltooptions", + "notes": [{ + "name": "MDN docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo" + }], + "authors": ["Oliver Tušla (@asmarcz)", "Chris Smith (@chris13524)"] +} +!*/ +/*! +{ + "name": "ServiceWorker API", + "property": "serviceworker", + "caniuse": "serviceworkers", + "notes": [{ + "name": "ServiceWorkers Explained", + "href": "https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md" + }] +} +!*/ +/*! +{ + "property": "speechrecognition", + "caniuse": "speech-recognition", + "tags": ["input", "speech"], + "authors": ["Cătălin Mariș"], + "name": "Speech Recognition API", + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/speech-api/speechapi.html#speechreco-section" + }, { + "name": "Introduction to the Web Speech API", + "href": "https://developers.google.com/web/updates/2013/01/Voice-Driven-Web-Apps-Introduction-to-the-Web-Speech-API" + }] +} +!*/ +/*! +{ + "property": "speechsynthesis", + "caniuse": "speech-synthesis", + "tags": ["input", "speech"], + "authors": ["Cătălin Mariș"], + "name": "Speech Synthesis API", + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/speech-api/speechapi.html#tts-section" + }] +} +!*/ +/*! +{ + "name": "Cookies", + "property": "cookies", + "tags": ["storage"], + "authors": ["tauren"] +} +!*/ +/*! +{ + "name": "IndexedDB", + "property": "indexeddb", + "caniuse": "indexeddb", + "tags": ["storage"], + "polyfills": ["indexeddb"], + "async": true +} +!*/ +/*! +{ + "name": "IndexedDB Blob", + "property": "indexeddbblob", + "tags": ["storage"] +} +!*/ +/*! +{ + "name": "IndexedDB 2.0", + "property": "indexeddb2", + "tags": ["storage"], + "caniuse": "indexeddb2", + "authors": ["Tan Zhen Yong (@Xenonym)"], + "polyfills": ["indexeddb"], + "async": true +} +!*/ +/*! +{ + "name": "Local Storage", + "property": "localstorage", + "caniuse": "namevalue-storage", + "tags": ["storage"], + "polyfills": [ + "joshuabell-polyfill", + "cupcake", + "storagepolyfill", + "amplifyjs", + "yui-cacheoffline" + ] +} +!*/ +/*! +{ + "name": "Quota Storage Management API", + "property": "quotamanagement", + "tags": ["storage"], + "builderAliases": ["quota_management_api"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/quota-api/" + }] +} +!*/ +/*! +{ + "name": "Session Storage", + "property": "sessionstorage", + "tags": ["storage"], + "polyfills": ["joshuabell-polyfill", "cupcake", "storagepolyfill"] +} +!*/ +/*! +{ + "name": "IE User Data API", + "property": "userdata", + "tags": ["storage"], + "authors": ["@stereobooster"], + "notes": [{ + "name": "MSDN Documentation", + "href": "https://msdn.microsoft.com/en-us/library/ms531424.aspx" + }] +} +!*/ +/*! +{ + "name": "Web SQL Database", + "property": "websqldatabase", + "caniuse": "sql-storage", + "tags": ["storage"] +} +!*/ +/*! +{ + "name": "style[scoped]", + "property": "stylescoped", + "caniuse": "style-scoped", + "tags": ["dom"], + "builderAliases": ["style_scoped"], + "authors": ["Cătălin Mariș"], + "notes": [{ + "name": "WHATWG Spec", + "href": "https://html.spec.whatwg.org/multipage/semantics.html#attr-style-scoped" + }], + "polyfills": ["scoped-styles"] +} +!*/ +/*! +{ + "name": "SVG", + "property": "svg", + "caniuse": "svg", + "tags": ["svg"], + "authors": ["Erik Dahlstrom"], + "polyfills": [ + "svgweb", + "raphael", + "canvg", + "svg-boilerplate", + "sie", + "fabricjs" + ] +} +!*/ +/*! +{ + "name": "SVG as an tag source", + "property": "svgasimg", + "caniuse": "svg-img", + "tags": ["svg"], + "aliases": ["svgincss"], + "authors": ["Chris Coyier"], + "notes": [{ + "name": "HTML5 Spec", + "href": "https://www.w3.org/TR/html5/embedded-content-0.html#the-img-element" + }] +} +!*/ +/*! +{ + "name": "SVG clip paths", + "property": "svgclippaths", + "tags": ["svg"], + "notes": [{ + "name": "Demo", + "href": "http://srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg" + }] +} +!*/ +/*! +{ + "name": "SVG filters", + "property": "svgfilters", + "caniuse": "svg-filters", + "tags": ["svg"], + "builderAliases": ["svg_filters"], + "authors": ["Erik Dahlstrom"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/SVG11/filters.html" + }] +} +!*/ +/*! +{ + "name": "SVG foreignObject", + "property": "svgforeignobject", + "tags": ["svg"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/SVG11/extend.html" + }] +} +!*/ +/*! +{ + "name": "Inline SVG", + "property": "inlinesvg", + "caniuse": "svg-html5", + "tags": ["svg"], + "notes": [{ + "name": "Test page", + "href": "https://paulirish.com/demo/inline-svg" + }, { + "name": "Test page and results", + "href": "https://codepen.io/eltonmesquita/full/GgXbvo/" + }], + "polyfills": ["inline-svg-polyfill"], + "knownBugs": ["False negative on some Chromia browsers."] +} +!*/ +/*! +{ + "name": "SVG SMIL animation", + "property": "smil", + "caniuse": "svg-smil", + "tags": ["svg"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/AudioVideo/" + }] +} +!*/ +/*! +{ + "name": "textarea maxlength", + "property": "textareamaxlength", + "aliases": ["textarea-maxlength"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea" + }], + "polyfills": ["maxlength"] +} +!*/ +/*! +{ + "name": "Text Encoding/Decoding", + "property": ["textencoder", "textdecoder"], + "caniuse" : "textencoder", + "notes": [{ + "name": "MDN TextEncoder Doc", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder" + }, { + "name": "MDN TextDecoder Doc", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder" + }], + "authors": ["dabretin"] +} +!*/ +/*! +{ + "name": "Typed arrays", + "property": "typedarrays", + "caniuse": "typedarrays", + "tags": ["js"], + "authors": ["Stanley Stuart (@fivetanley)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Typed_arrays" + }, { + "name": "Kronos spec", + "href": "http://www.ecma-international.org/ecma-262/6.0/#sec-typedarray-objects" + }], + "polyfills": ["joshuabell-polyfill"] +} +!*/ +/*! +{ + "name": "Unicode Range", + "property": "unicoderange", + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/2013/CR-css-fonts-3-20131003/#descdef-unicode-range" + }, { + "name": "24 Way article", + "href": "https://24ways.org/2011/creating-custom-font-stacks-with-unicode-range" + }] +} +!*/ +/*! +{ + "name": "Blob URLs", + "property": "bloburls", + "caniuse": "bloburls", + "notes": [{ + "name": "W3C Working Draft Spec", + "href": "https://www.w3.org/TR/FileAPI/#creating-revoking" + }], + "tags": ["file", "url"], + "authors": ["Ron Waldon (@jokeyrhyme)"] +} +!*/ +/*! +{ + "name": "Data URI", + "property": "datauri", + "caniuse": "datauri", + "tags": ["url"], + "builderAliases": ["url_data_uri"], + "async": true, + "notes": [{ + "name": "Wikipedia article", + "href": "https://en.wikipedia.org/wiki/Data_URI_scheme" + }], + "warnings": ["Support in Internet Explorer 8 is limited to images and linked resources like CSS files, not HTML files"] +} +!*/ +/*! +{ + "name": "URL parser", + "property": "urlparser", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://url.spec.whatwg.org/" + }], + "polyfills": ["urlparser"], + "authors": ["Ron Waldon (@jokeyrhyme)"], + "tags": ["url"] +} +!*/ +/*! +{ + "property": "urlsearchparams", + "caniuse": "urlsearchparams", + "tags": ["querystring", "url"], + "authors": ["Cătălin Mariș"], + "name": "URLSearchParams API", + "notes": [{ + "name": "WHATWG Spec", + "href": "https://url.spec.whatwg.org/#interface-urlsearchparams" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams" + }] +} +!*/ +/*! +{ + "name": "Vibration API", + "property": "vibrate", + "caniuse": "vibration", + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en/DOM/window.navigator.mozVibrate" + }, { + "name": "W3C Spec", + "href": "https://www.w3.org/TR/vibration/" + }] +} +!*/ +/*! +{ + "name": "HTML5 Video", + "property": "video", + "caniuse": "video", + "tags": ["html5", "video", "media"], + "knownBugs": ["Without QuickTime, `Modernizr.video.h264` will be `undefined`; https://github.com/Modernizr/Modernizr/issues/546"], + "polyfills": [ + "html5media", + "mediaelementjs", + "videojs", + "leanbackplayer", + "videoforeverybody" + ] +} +!*/ +/*! +{ + "name": "Video Autoplay", + "property": "videoautoplay", + "tags": ["video"], + "async": true, + "warnings": ["This test is very large – only include it if you absolutely need it"], + "knownBugs": ["crashes with an alert on iOS7 when added to homescreen"] +} +!*/ +/*! +{ + "name": "Video crossOrigin", + "property": "videocrossorigin", + "caniuse": "cors", + "authors": ["Florian Mailliet"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes" + }] +} +!*/ +/*! +{ + "name": "Video Loop Attribute", + "property": "videoloop", + "tags": ["video", "media"] +} +!*/ +/*! +{ + "name": "Video Preload Attribute", + "property": "videopreload", + "tags": ["video", "media"] +} +!*/ +/*! +{ + "name": "VML", + "property": "vml", + "tags": ["vml"], + "authors": ["Craig Andrews (@candrews)"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/NOTE-VML" + }, { + "name": "MSDN Documentation", + "href": "https://docs.microsoft.com/en-us/windows/desktop/VML/msdn-online-vml-introduction" + }] +} +!*/ +/*! +{ + "name": "Web Intents", + "property": "webintents", + "authors": ["Eric Bidelman"], + "notes": [{ + "name": "Web Intents project site", + "href": "http://www.webintents.org/" + }], + "builderAliases": ["web_intents"] +} +!*/ +/*! +{ + "name": "Web Animation API", + "property": "webanimations", + "caniuse": "web-animation", + "tags": ["webanimations"], + "polyfills": ["webanimationsjs"], + "notes": [{ + "name": "Introducing Web Animations", + "href": "https://birtles.wordpress.com/2013/06/26/introducing-web-animations/" + }] +} +!*/ +/*! +{ + "name": "PublicKeyCredential", + "notes": [ + { + "name": "MDN Documentation", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential" + }, + { + "name": "Google Developers solution", + "href": "https://developers.google.com/web/updates/2018/03/webauthn-credential-management#the_solution" + } + ], + "property": "publickeycredential", + "tags": ["webauthn", "web authentication"], + "authors": ["Eric Delia"] +} +!*/ +/*! +{ + "name": "WebGL", + "property": "webgl", + "caniuse": "webgl", + "tags": ["webgl", "graphics"], + "polyfills": ["jebgl", "cwebgl", "iewebgl"] +} +!*/ +/*! +{ + "name": "WebGL Extensions", + "property": "webglextensions", + "tags": ["webgl", "graphics"], + "builderAliases": ["webgl_extensions"], + "async": true, + "authors": ["Ilmari Heikkinen"], + "notes": [{ + "name": "Kronos extensions registry", + "href": "https://www.khronos.org/registry/webgl/extensions/" + }] +} +!*/ +/*! +{ + "name": "RTC Peer Connection", + "property": "peerconnection", + "caniuse": "rtcpeerconnection", + "tags": ["webrtc"], + "authors": ["Ankur Oberoi"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/webrtc/" + }] +} +!*/ +/*! +{ + "name": "RTC Data Channel", + "property": "datachannel", + "notes": [{ + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/webrtc/datachannels/" + }] +} +!*/ +/*! +{ + "name": "getUserMedia", + "property": "getusermedia", + "caniuse": "stream", + "tags": ["webrtc"], + "authors": ["Eric Bidelman", "Masataka Yakura"], + "notes": [{ + "name": "W3C Spec", + "href": "https://w3c.github.io/mediacapture-main/#dom-mediadevices-getusermedia" + }] +} +!*/ +/*! +{ + "name": "MediaStream Recording API", + "property": "mediarecorder", + "caniuse": "mediarecorder", + "tags": ["mediarecorder", "media"], + "authors": ["Onkar Dahale"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/MediaStream_Recording_API" + }] +} +!*/ +/*! +{ + "name": "WebSockets Support", + "property": "websockets", + "authors": ["Phread (@fearphage)", "Mike Sherov (@mikesherov)", "Burak Yigit Kaya (@BYK)"], + "caniuse": "websockets", + "tags": ["html5"], + "knownBugs": ["This test will reject any old version of WebSockets even if it is not prefixed such as in Safari 5.1"], + "notes": [{ + "name": "CLOSING State and Spec", + "href": "https://www.w3.org/TR/websockets/#the-websocket-interface" + }], + "polyfills": [ + "sockjs", + "socketio", + "websocketjs", + "atmosphere", + "graceful-websocket", + "portal", + "datachannel" + ] +} +!*/ +/*! +{ + "name": "Binary WebSockets", + "property": "websocketsbinary", + "tags": ["websockets"], + "builderAliases": ["websockets_binary"] +} +!*/ +/*! +{ + "name": "Base 64 encoding/decoding", + "property": "atobbtoa", + "builderAliases": ["atob-btoa"], + "caniuse": "atob-btoa", + "tags": ["atob", "base64", "WindowBase64", "btoa"], + "authors": ["Christian Ulbrich"], + "notes": [{ + "name": "WindowBase64", + "href": "https://www.w3.org/TR/html5/webappapis.html#windowbase64" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob" + }], + "polyfills": ["base64js"] +} +!*/ +/*! +{ + "name": "Framed window", + "property": "framed", + "tags": ["window"], + "builderAliases": ["window_framed"] +} +!*/ +/*! +{ + "name": "matchMedia", + "property": "matchmedia", + "caniuse": "matchmedia", + "tags": ["matchmedia"], + "authors": ["Alberto Elias"], + "notes": [{ + "name": "W3C Spec", + "href": "https://drafts.csswg.org/cssom-view/#the-mediaquerylist-interface" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Window.matchMedia" + }], + "polyfills": ["matchmediajs"] +} +!*/ +/*! +{ + "name": "PushManager", + "property": "pushmanager", + "caniuse": "mdn-api_pushmanager", + "authors": ["Dawid Kulpa (@dawidkulpa)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/PushManager" + }] +} +!*/ +/*! +{ + "name": "ResizeObserver", + "property": "resizeobserver", + "caniuse": "resizeobserver", + "tags": ["ResizeObserver"], + "authors": ["Christian Andersson"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/resize-observer/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver" + }, { + "name": "Web.dev Article", + "href": "https://web.dev/resize-observer/" + }, { + "name": "Digital Ocean tutorial", + "href": "https://www.digitalocean.com/community/tutorials/js-resize-observer" + }] +} +!*/ +/*! +{ + "name": "worker type option test", + "property": "workertypeoption", + "caniuse":"mdn-api_worker_worker_ecmascript_modules", + "tags": ["web worker type options", "web worker"], + "builderAliases": ["worker_type_options"], + "authors": ["Debadutta Panda"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Worker/Worker" + }] +} +!*/ +/*! +{ + "name": "Workers from Blob URIs", + "property": "blobworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_blobworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }], + "warnings": ["This test may output garbage to console."], + "authors": ["Jussi Kalliokoski"], + "async": true +} +!*/ +/*! +{ + "name": "Workers from Data URIs", + "property": "dataworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_dataworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }], + "warnings": ["This test may output garbage to console."], + "authors": ["Jussi Kalliokoski"], + "async": true +} +!*/ +/*! +{ + "name": "Shared Workers", + "property": "sharedworkers", + "caniuse": "sharedworkers", + "tags": ["performance", "workers"], + "builderAliases": ["workers_sharedworkers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }] +} +!*/ +/*! +{ + "name": "Web Workers", + "property": "webworkers", + "caniuse": "webworkers", + "tags": ["performance", "workers"], + "notes": [{ + "name": "W3C Spec", + "href": "https://www.w3.org/TR/workers/" + }, { + "name": "HTML5 Rocks Tutorial", + "href": "https://www.html5rocks.com/en/tutorials/workers/basics/" + }, { + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers" + }], + "polyfills": ["fakeworker", "html5shims"] +} +!*/ +/*! +{ + "name": "Transferables Objects", + "property": "transferables", + "tags": ["performance", "workers"], + "builderAliases": ["transferables"], + "notes": [{ + "name": "Transferable Objects: Lightning Fast!", + "href": "https://developers.google.com/web/updates/2011/12/Transferable-Objects-Lightning-Fast" + }], + "async": true +} +!*/ +/*! +{ + "name": "XDomainRequest", + "property": "xdomainrequest", + "tags": ["cors", "xdomainrequest", "ie9", "ie8"], + "authors": ["Ivan Pan (@hypotenuse)"], + "notes": [{ + "name": "MDN Docs", + "href": "https://developer.mozilla.org/en-US/docs/Web/API/XDomainRequest" + }] +} +!*/ diff --git a/tests/assets/modernizr/roll.sh b/tests/assets/modernizr/roll.sh new file mode 100644 index 0000000000..8e6dd5c651 --- /dev/null +++ b/tests/assets/modernizr/roll.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +MODERNIZR_VERSION="44fa7b07c367a1814e8699e3a2f15c53fbe32df7" + +cd "$(dirname "$0")" + +rm -rf Modernizr +git clone https://github.com/Modernizr/Modernizr +cd Modernizr +git checkout $MODERNIZR_VERSION +npm ci + +# Modernizr minifier is not working, hence we minify with ESBuild. +./bin/modernizr --config lib/config-all.json +npx esbuild --bundle modernizr.js --minify --outfile=../modernizr.js + +cd .. +rm -rf Modernizr diff --git a/tests/assets/modernizr/safari-14-1.json b/tests/assets/modernizr/safari-18.json similarity index 65% rename from tests/assets/modernizr/safari-14-1.json rename to tests/assets/modernizr/safari-18.json index 5d39648297..e4f4aa48b3 100644 --- a/tests/assets/modernizr/safari-14-1.json +++ b/tests/assets/modernizr/safari-18.json @@ -11,6 +11,249 @@ "required": true, "step": true }, + "adownload": true, + "aping": true, + "areaping": true, + "ambientlight": false, + "applicationcache": false, + "audio": { + "ogg": "", + "mp3": "probably", + "opus": "probably", + "wav": "probably", + "m4a": "maybe" + }, + "audioloop": true, + "webaudio": true, + "batteryapi": false, + "battery-api": false, + "lowbattery": false, + "blobconstructor": true, + "blob-constructor": true, + "broadcastchannel": true, + "canvas": true, + "canvasblending": true, + "todataurljpeg": true, + "todataurlpng": true, + "todataurlwebp": false, + "canvaswinding": true, + "canvastext": true, + "clipboard": { + "read": true, + "readtext": true, + "write": true, + "writetext": true + }, + "contenteditable": true, + "contextmenu": false, + "cors": true, + "crypto": true, + "getrandomvalues": true, + "cssall": true, + "cssanimations": true, + "appearance": true, + "aspectratio": true, + "backdropfilter": true, + "backgroundblendmode": true, + "backgroundcliptext": true, + "bgpositionshorthand": true, + "bgpositionxy": true, + "bgrepeatround": true, + "bgrepeatspace": true, + "backgroundsize": true, + "bgsizecover": true, + "borderimage": true, + "borderradius": true, + "boxdecorationbreak": true, + "boxshadow": true, + "boxsizing": true, + "csscalc": true, + "checked": true, + "csschunit": true, + "csscolumns": { + "width": true, + "span": true, + "fill": true, + "gap": true, + "rule": true, + "rulecolor": true, + "rulestyle": true, + "rulewidth": true, + "breakbefore": true, + "breakafter": true, + "breakinside": true + }, + "cssgridlegacy": false, + "cssgrid": true, + "cubicbezierrange": true, + "customproperties": true, + "displayrunin": false, + "display-runin": false, + "displaytable": true, + "display-table": true, + "ellipsis": true, + "cssescape": true, + "cssexunit": true, + "supports": true, + "cssfilters": true, + "flexbox": true, + "flexboxlegacy": true, + "flexboxtweener": false, + "flexgap": true, + "flexwrap": true, + "focusvisible": true, + "focuswithin": true, + "fontdisplay": true, + "fontface": true, + "generatedcontent": true, + "cssgradients": true, + "hairline": true, + "hsla": true, + "cssinvalid": true, + "lastchild": true, + "cssmask": true, + "mediaqueries": true, + "multiplebgs": true, + "nthchild": true, + "objectfit": true, + "object-fit": true, + "opacity": true, + "overflowscrolling": false, + "csspointerevents": true, + "csspositionsticky": true, + "csspseudoanimations": true, + "csstransitions": true, + "csspseudotransitions": true, + "cssreflections": true, + "regions": false, + "cssremunit": true, + "cssresize": true, + "rgba": true, + "cssscrollbar": true, + "scrollsnappoints": true, + "shapes": true, + "siblinggeneral": true, + "subpixelfont": true, + "target": true, + "textalignlast": true, + "textdecoration": { + "line": true, + "style": true, + "color": true, + "skip": true, + "skipink": true + }, + "textshadow": true, + "csstransforms": true, + "csstransforms3d": true, + "csstransformslevel2": true, + "preserve3d": true, + "userselect": true, + "cssvalid": true, + "variablefonts": true, + "cssvhunit": true, + "cssvmaxunit": true, + "cssvminunit": true, + "cssvwunit": true, + "willchange": true, + "wrapflow": false, + "customelements": true, + "customprotocolhandler": false, + "dart": false, + "dataview": true, + "classlist": true, + "createelementattrs": false, + "createelement-attrs": false, + "dataset": true, + "documentfragment": true, + "hidden": true, + "intersectionobserver": true, + "microdata": false, + "mutationobserver": true, + "passiveeventlisteners": true, + "shadowroot": true, + "shadowrootlegacy": false, + "bdi": true, + "details": true, + "outputelem": true, + "picture": true, + "progressbar": true, + "meter": true, + "ruby": true, + "template": true, + "time": false, + "texttrackapi": true, + "track": true, + "unknownelements": true, + "emoji": true, + "es5array": true, + "es5date": true, + "es5function": true, + "es5object": true, + "strictmode": true, + "es5string": true, + "json": true, + "es5syntax": true, + "es5undefined": true, + "es5": true, + "es6array": true, + "arrow": true, + "es6class": true, + "es6collections": true, + "generators": true, + "es6math": true, + "es6number": true, + "es6object": true, + "promises": true, + "restparameters": true, + "spreadarray": true, + "stringtemplate": true, + "es6string": true, + "es6symbol": true, + "es7array": true, + "restdestructuringarray": true, + "restdestructuringobject": true, + "spreadobject": true, + "es8object": true, + "customevent": true, + "devicemotion": true, + "deviceorientation": true, + "eventlistener": true, + "forcetouch": false, + "hashchange": true, + "oninput": true, + "pointerevents": true, + "proximity": false, + "filereader": true, + "filesystem": false, + "flash": false, + "fullscreen": true, + "gamepads": true, + "geolocation": true, + "hiddenscroll": true, + "history": true, + "htmlimports": false, + "ie8compat": false, + "sandbox": true, + "seamless": false, + "srcdoc": true, + "imgcrossorigin": true, + "lazyloading": true, + "sizes": true, + "srcset": true, + "capture": false, + "fileinput": true, + "fileinputdirectory": true, + "inputformaction": true, + "input-formaction": true, + "formattribute": true, + "inputformenctype": true, + "input-formenctype": true, + "inputformmethod": true, + "inputformnovalidate": true, + "input-formnovalidate": true, + "inputformtarget": true, + "input-formtarget": true, "inputtypes": { "search": true, "tel": true, @@ -26,278 +269,148 @@ "range": true, "color": true }, - "htmlimports": false, - "history": true, - "ie8compat": false, - "applicationcache": false, - "blobconstructor": true, - "blob-constructor": true, - "cookies": true, - "cors": true, - "customelements": true, - "customprotocolhandler": false, - "customevent": true, - "dataview": true, - "eventlistener": true, - "geolocation": true, - "json": true, + "formvalidation": true, + "localizednumber": false, + "inputsearchevent": false, + "placeholder": true, + "requestautocomplete": false, + "intl": true, + "ligatures": true, + "olreversed": true, + "mathml": true, + "mediasource": true, + "hovermq": true, + "pointermq": true, "messagechannel": true, - "notification": true, - "postmessage": true, - "queryselector": true, - "serviceworker": true, - "svg": true, - "templatestrings": true, - "typedarrays": true, - "websockets": true, - "xdomainrequest": false, - "webaudio": true, - "cssescape": true, - "focuswithin": true, - "supports": true, - "target": true, - "microdata": false, - "mutationobserver": true, - "passiveeventlisteners": true, - "picture": true, - "es5array": true, - "es5date": true, - "es5function": true, "beacon": true, + "effectivetype": false, "lowbandwidth": false, "eventsource": true, "fetch": true, - "xhrresponsetype": true, - "xhr2": true, - "speechsynthesis": true, - "localstorage": true, - "sessionstorage": true, - "websqldatabase": true, - "es5object": true, - "svgfilters": true, - "strictmode": true, - "es5string": true, - "es5syntax": true, - "es5undefined": true, - "es5": true, - "es6array": true, - "arrow": true, - "es6collections": true, - "generators": true, - "es6math": true, - "es6number": true, - "es6object": true, - "promises": true, - "es6string": true, - "devicemotion": false, - "devicemotion2": false, - "deviceorientation": false, - "deviceorientation2": false, - "deviceorientation3": false, - "filereader": true, - "urlparser": true, - "urlsearchparams": true, - "framed": false, - "webworkers": true, - "contextmenu": false, - "cssall": true, - "willchange": true, - "classlist": true, - "documentfragment": true, - "contains": false, - "audio": true, - "canvas": true, - "canvastext": true, - "contenteditable": true, - "emoji": true, - "olreversed": true, - "userdata": false, - "video": true, - "vml": false, - "webanimations": true, - "webgl": true, - "adownload": true, - "audioloop": true, - "canvasblending": true, - "todataurljpeg": true, - "todataurlpng": true, - "todataurlwebp": false, - "canvaswinding": true, - "bgpositionshorthand": true, - "multiplebgs": true, - "csspointerevents": true, - "cssremunit": true, - "rgba": true, - "preserve3d": true, - "createelementattrs": false, - "createelement-attrs": false, - "dataset": true, - "hidden": true, - "outputelem": true, - "progressbar": true, - "meter": true, - "ruby": true, - "template": true, - "srcset": true, - "time": false, - "texttrackapi": true, - "track": true, - "unknownelements": true, - "inputformaction": true, - "input-formaction": true, - "inputformenctype": true, - "input-formenctype": true, - "inputformmethod": true, - "inputformtarget": false, - "input-formtarget": false, - "scriptasync": true, - "scriptdefer": true, - "stylescoped": false, - "capture": false, - "fileinput": true, - "formattribute": true, - "placeholder": true, - "sandbox": true, - "inlinesvg": true, - "textareamaxlength": true, - "videocrossorigin": true, - "webglextensions": true, - "seamless": false, - "srcdoc": true, - "imgcrossorigin": true, - "hashchange": true, - "inputsearchevent": false, - "ambientlight": false, - "datalistelem": true, - "videoloop": true, - "csscalc": true, - "cubicbezierrange": true, - "cssgradients": true, - "opacity": true, - "csspositionsticky": true, - "csschunit": true, - "cssexunit": true, - "hsla": true, - "videopreload": true, - "getusermedia": true, - "websocketsbinary": true, - "atobbtoa": true, - "atob-btoa": true, - "sharedworkers": true, - "bdi": true, "xhrresponsetypearraybuffer": true, "xhrresponsetypeblob": true, "xhrresponsetypedocument": true, "xhrresponsetypejson": true, "xhrresponsetypetext": true, - "svgclippaths": true, - "svgforeignobject": true, - "smil": true, - "hiddenscroll": true, - "mathml": true, - "touchevents": false, - "unicoderange": true, - "unicode": true, - "checked": true, - "displaytable": true, - "display-table": true, - "fontface": true, - "generatedcontent": true, - "hairline": true, - "cssinvalid": true, - "lastchild": true, - "nthchild": true, - "cssscrollbar": true, - "siblinggeneral": true, - "subpixelfont": true, - "cssvalid": true, - "cssvhunit": true, - "cssvmaxunit": true, - "cssvminunit": true, - "cssvwunit": true, - "details": true, - "oninput": true, - "formvalidation": true, - "localizednumber": false, - "mediaqueries": true, - "flash": false, - "proximity": false, - "sizes": true, - "hovermq": true, - "pointermq": true, - "svgasimg": true, - "pointerevents": true, - "fileinputdirectory": true, - "textshadow": true, - "batteryapi": false, - "battery-api": false, - "crypto": true, - "dart": false, - "forcetouch": false, - "fullscreen": true, - "gamepads": true, - "intl": true, + "xhrresponsetype": true, + "xhr2": true, + "notification": true, "pagevisibility": true, "performance": true, "pointerlock": true, - "quotamanagement": false, + "postmessage": { + "structuredclones": true + }, + "proxy": true, + "queryselector": true, + "prefetch": false, "requestanimationframe": true, "raf": true, - "vibrate": false, - "webintents": false, - "lowbattery": false, - "getrandomvalues": true, - "backgroundblendmode": true, - "objectfit": true, - "object-fit": true, - "regions": false, - "wrapflow": false, + "scriptasync": true, + "scriptdefer": true, + "scrolltooptions": false, + "serviceworker": true, "speechrecognition": true, - "filesystem": false, - "requestautocomplete": false, + "speechsynthesis": true, + "cookies": true, + "localstorage": true, + "quotamanagement": false, + "sessionstorage": true, + "userdata": false, + "websqldatabase": true, + "stylescoped": false, + "svg": true, + "svgasimg": true, + "svgclippaths": true, + "svgfilters": true, + "svgforeignobject": true, + "inlinesvg": true, + "smil": true, + "textareamaxlength": true, + "textencoder": true, + "textdecoder": true, + "typedarrays": true, + "unicoderange": true, "bloburls": true, - "transferables": true, + "urlparser": true, + "urlsearchparams": true, + "vibrate": false, + "video": { + "ogg": "", + "h264": "probably", + "h265": "", + "webm": "probably", + "vp9": "probably", + "hls": "probably", + "av1": "" + }, + "videocrossorigin": true, + "videoloop": true, + "videopreload": true, + "vml": false, + "webintents": false, + "webanimations": true, + "publickeycredential": true, + "webgl": true, + "webglextensions": { + "ANGLE_instanced_arrays": true, + "EXT_blend_minmax": true, + "EXT_clip_control": true, + "EXT_color_buffer_half_float": true, + "EXT_depth_clamp": true, + "EXT_float_blend": true, + "EXT_frag_depth": true, + "EXT_polygon_offset_clamp": true, + "EXT_shader_texture_lod": true, + "EXT_texture_compression_bptc": true, + "EXT_texture_compression_rgtc": true, + "EXT_texture_filter_anisotropic": true, + "EXT_texture_mirror_clamp_to_edge": true, + "EXT_sRGB": true, + "KHR_parallel_shader_compile": true, + "OES_element_index_uint": true, + "OES_fbo_render_mipmap": true, + "OES_standard_derivatives": true, + "OES_texture_float": true, + "OES_texture_float_linear": true, + "OES_texture_half_float": true, + "OES_texture_half_float_linear": true, + "OES_vertex_array_object": true, + "WEBGL_blend_func_extended": true, + "WEBGL_color_buffer_float": true, + "WEBGL_compressed_texture_astc": true, + "WEBGL_compressed_texture_etc": true, + "WEBGL_compressed_texture_etc1": true, + "WEBGL_compressed_texture_pvrtc": true, + "WEBKIT_WEBGL_compressed_texture_pvrtc": true, + "WEBGL_compressed_texture_s3tc": true, + "WEBGL_compressed_texture_s3tc_srgb": true, + "WEBGL_debug_renderer_info": true, + "WEBGL_debug_shaders": true, + "WEBGL_depth_texture": true, + "WEBGL_draw_buffers": true, + "WEBGL_lose_context": true, + "WEBGL_multi_draw": true, + "WEBGL_polygon_mode": true + }, "peerconnection": true, - "datachannel": false, + "datachannel": true, + "getusermedia": true, + "mediastream": true, + "websockets": true, + "websocketsbinary": true, + "atobbtoa": true, + "atob-btoa": true, + "framed": false, "matchmedia": true, - "ligatures": true, - "cssanimations": true, - "csspseudoanimations": true, - "appearance": true, - "backdropfilter": true, - "backgroundcliptext": true, - "bgpositionxy": true, - "bgrepeatround": true, - "bgrepeatspace": true, - "backgroundsize": true, - "bgsizecover": true, - "borderimage": true, - "borderradius": true, - "boxshadow": true, - "boxsizing": true, - "csscolumns": true, - "cssgridlegacy": false, - "cssgrid": true, - "displayrunin": false, - "display-runin": false, - "ellipsis": true, - "cssfilters": true, - "flexbox": true, - "flexboxlegacy": true, - "flexboxtweener": false, - "flexwrap": true, - "cssmask": true, - "overflowscrolling": false, - "cssreflections": true, - "cssresize": true, - "scrollsnappoints": true, - "shapes": true, - "textalignlast": true, - "csstransforms": true, - "csstransforms3d": true, - "csstransformslevel2": true, - "csstransitions": true, - "csspseudotransitions": true, - "userselect": true, - "variablefonts": true -} + "pushmanager": true, + "resizeobserver": true, + "workertypeoption": true, + "sharedworkers": true, + "webworkers": true, + "transferables": true, + "xdomainrequest": false, + "devicemotion2": true, + "deviceorientation2": false, + "deviceorientation3": true +} \ No newline at end of file diff --git a/tests/assets/network-tab/network.html b/tests/assets/network-tab/network.html index d46ff846dc..32f7d2cf6c 100644 --- a/tests/assets/network-tab/network.html +++ b/tests/assets/network-tab/network.html @@ -13,6 +13,25 @@ +

Network Tab Test

diff --git a/tests/config/browserTest.ts b/tests/config/browserTest.ts index 01b574f105..b5b09fbae7 100644 --- a/tests/config/browserTest.ts +++ b/tests/config/browserTest.ts @@ -59,10 +59,8 @@ const test = baseTest.extend }, { scope: 'worker' }], allowsThirdParty: [async ({ browserName, browserMajorVersion, channel }, run) => { - if (browserName === 'firefox' && !channel) - await run(browserMajorVersion >= 103); - else if (browserName === 'firefox' && channel === 'firefox-beta') - await run(browserMajorVersion < 103 || browserMajorVersion >= 110); + if (browserName === 'firefox') + await run(true); else await run(false); }, { scope: 'worker' }], @@ -74,10 +72,8 @@ const test = baseTest.extend await run('Lax'); else if (browserName === 'webkit' && !isLinux) await run('None'); - else if (browserName === 'firefox' && channel === 'firefox-beta') - await run(browserMajorVersion >= 103 && browserMajorVersion < 110 ? 'Lax' : 'None'); - else if (browserName === 'firefox' && channel !== 'firefox-beta') - await run(browserMajorVersion >= 103 ? 'None' : 'Lax'); + else if (browserName === 'firefox') + await run('None'); else throw new Error('unknown browser - ' + browserName); }, { scope: 'worker' }], diff --git a/tests/library/browsercontext-proxy.spec.ts b/tests/library/browsercontext-proxy.spec.ts index c2d9d5b31c..466e866e04 100644 --- a/tests/library/browsercontext-proxy.spec.ts +++ b/tests/library/browsercontext-proxy.spec.ts @@ -141,7 +141,6 @@ it.describe('should proxy local network requests', () => { it('should use ipv6 proxy', async ({ contextFactory, server, proxyServer, browserName }) => { it.fail(browserName === 'firefox', 'page.goto: NS_ERROR_UNKNOWN_HOST'); - it.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default'); proxyServer.forwardTo(server.PORT); const context = await contextFactory({ proxy: { server: `[0:0:0:0:0:0:0:1]:${proxyServer.PORT}` } diff --git a/tests/library/browsercontext-service-worker-policy.spec.ts b/tests/library/browsercontext-service-worker-policy.spec.ts index 213cf1461a..1923a2dc4e 100644 --- a/tests/library/browsercontext-service-worker-policy.spec.ts +++ b/tests/library/browsercontext-service-worker-policy.spec.ts @@ -29,4 +29,12 @@ it.describe('block', () => { page.goto(server.PREFIX + '/serviceworkers/empty/sw.html'), ]); }); + + it('should not throw error on about:blank', async ({ page }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32292' }); + const errors = []; + page.on('pageerror', error => errors.push(error)); + await page.goto('about:blank'); + expect(errors).toEqual([]); + }); }); diff --git a/tests/library/browsercontext-viewport-mobile.spec.ts b/tests/library/browsercontext-viewport-mobile.spec.ts index 1f897f60fa..2e0f90ceb0 100644 --- a/tests/library/browsercontext-viewport-mobile.spec.ts +++ b/tests/library/browsercontext-viewport-mobile.spec.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import os from 'os'; import { browserTest as it, expect } from '../config/browserTest'; it.describe('mobile viewport', () => { @@ -55,23 +54,19 @@ it.describe('mobile viewport', () => { } }); - it('should be detectable by Modernizr', async ({ playwright, browser, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); + it('should be detectable', async ({ playwright, browser, server, browserName, platform }) => { const iPhone = playwright.devices['iPhone 6']; const context = await browser.newContext({ ...iPhone }); const page = await context.newPage(); - await page.goto(server.PREFIX + '/detect-touch.html'); - expect(await page.evaluate(() => document.body.textContent!.trim())).toBe('YES'); + expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true); await context.close(); }); it('should detect touch when applying viewport with touches', async ({ browser, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const context = await browser.newContext({ viewport: { width: 800, height: 600 }, hasTouch: true }); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); - await page.addScriptTag({ url: server.PREFIX + '/modernizr.js' }); - expect(await page.evaluate(() => (window as any)['Modernizr'].touchevents)).toBe(true); + expect(await page.evaluate(() => 'ontouchstart' in window || !!window.TouchEvent)).toBe(true); await context.close(); }); diff --git a/tests/library/browsercontext-viewport.spec.ts b/tests/library/browsercontext-viewport.spec.ts index 4ba1e2f52d..abb14b3a71 100644 --- a/tests/library/browsercontext-viewport.spec.ts +++ b/tests/library/browsercontext-viewport.spec.ts @@ -94,11 +94,8 @@ it('should emulate availWidth and availHeight', async ({ page }) => { }); it('should not have touch by default', async ({ page, server, browserName, platform }) => { - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'detect-touch.html uses Modernizr which uses WebGL. WebGL is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); await page.goto(server.PREFIX + '/mobile.html'); expect(await page.evaluate(() => 'ontouchstart' in window)).toBe(false); - await page.goto(server.PREFIX + '/detect-touch.html'); - expect(await page.evaluate(() => document.body.textContent.trim())).toBe('NO'); }); it('should throw on tap if hasTouch is not enabled', async ({ page }) => { diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 807af5154c..75ca2468f7 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -601,8 +601,7 @@ test.describe('browser', () => { test('support http2', async ({ browser, startCCServer, asset, browserName }) => { test.skip(browserName === 'webkit' && process.platform === 'darwin', 'WebKit on macOS doesn\n proxy localhost'); - const enableHTTP1FallbackWhenUsingHttp2 = browserName === 'webkit' && process.platform === 'linux'; - const serverURL = await startCCServer({ http2: true, enableHTTP1FallbackWhenUsingHttp2 }); + const serverURL = await startCCServer({ http2: true }); const page = await browser.newPage({ ignoreHTTPSErrors: true, clientCertificates: [{ @@ -611,19 +610,16 @@ test.describe('browser', () => { keyPath: asset('client-certificates/client/trusted/key.pem'), }], }); - // TODO: We should investigate why http2 is not supported in WebKit on Linux. - // https://bugs.webkit.org/show_bug.cgi?id=276990 - const expectedProtocol = enableHTTP1FallbackWhenUsingHttp2 ? 'http/1.1' : 'h2'; { await page.goto(serverURL.replace('localhost', 'local.playwright')); await expect(page.getByTestId('message')).toHaveText('Sorry, but you need to provide a client certificate to continue.'); - await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol); + await expect(page.getByTestId('alpn-protocol')).toHaveText('h2'); await expect(page.getByTestId('servername')).toHaveText('local.playwright'); } { await page.goto(serverURL); await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); - await expect(page.getByTestId('alpn-protocol')).toHaveText(expectedProtocol); + await expect(page.getByTestId('alpn-protocol')).toHaveText('h2'); } await page.close(); }); diff --git a/tests/library/clock.spec.ts b/tests/library/clock.spec.ts index 90279fd893..daad405e70 100644 --- a/tests/library/clock.spec.ts +++ b/tests/library/clock.spec.ts @@ -75,6 +75,14 @@ it.describe('setTimeout', () => { }).toThrow(); }); + it('does not throw if |undefined| or |null| is passed as a callback', async ({ clock }) => { + const timerId1 = clock.setTimeout(undefined, 10); + const timerId2 = clock.setTimeout(null, 10); + await clock.runFor(10); + expect(timerId1).toBeGreaterThan(0); + expect(timerId2).toBeGreaterThan(timerId1); + }); + it('returns numeric id or object with numeric id', async ({ clock }) => { const result = clock.setTimeout(() => { }, 10); expect(result).toEqual(expect.any(Number)); @@ -761,6 +769,14 @@ it.describe('setInterval', () => { }).toThrow(); }); + it('does not throw if |undefined| or |null| is passed as a callback', async ({ clock }) => { + const timerId1 = clock.setInterval(undefined, 10); + const timerId2 = clock.setInterval(null, 10); + await clock.runFor(10); + expect(timerId1).toBeGreaterThan(0); + expect(timerId2).toBeGreaterThan(timerId1); + }); + it('returns numeric id or object with numeric id', async ({ clock }) => { const result = clock.setInterval(() => {}, 10); expect(result).toBeGreaterThan(0); diff --git a/tests/library/inspector/cli-codegen-3.spec.ts b/tests/library/inspector/cli-codegen-3.spec.ts index 424b2bacfe..6828576456 100644 --- a/tests/library/inspector/cli-codegen-3.spec.ts +++ b/tests/library/inspector/cli-codegen-3.spec.ts @@ -120,20 +120,20 @@ await page.GetByRole(AriaRole.Button, new() { Name = "Submit" }).Nth(1).ClickAsy frameHello1.click('text=Hello1'), ]); - expect(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').getByText('Hello1').click();`); + expect.soft(sources.get('JavaScript')!.text).toContain(` + await page.locator('#frame1').contentFrame().getByText('Hello1').click();`); - expect(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").getByText("Hello1").click();`); + expect.soft(sources.get('Java')!.text).toContain(` + page.locator("#frame1").contentFrame().getByText("Hello1").click();`); - expect(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").get_by_text("Hello1").click()`); + expect.soft(sources.get('Python')!.text).toContain(` + page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); - expect(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").get_by_text("Hello1").click()`); + expect.soft(sources.get('Python Async')!.text).toContain(` + await page.locator("#frame1").content_frame().get_by_text("Hello1").click()`); - expect(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").GetByText("Hello1").ClickAsync();`); + expect.soft(sources.get('C#')!.text).toContain(` +await page.Locator("#frame1").ContentFrame().GetByText("Hello1").ClickAsync();`); [sources] = await Promise.all([ @@ -142,19 +142,19 @@ await page.FrameLocator("#frame1").GetByText("Hello1").ClickAsync();`); ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').getByText('Hello2').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().getByText('Hello2').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").getByText("Hello2").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().getByText("Hello2").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").get_by_text("Hello2").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().get_by_text("Hello2").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").GetByText("Hello2").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().GetByText("Hello2").ClickAsync();`); [sources] = await Promise.all([ @@ -163,19 +163,19 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").GetByText("Hello2").Cl ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe[name="one"]').getByText('HelloNameOne').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe[name="one"]').contentFrame().getByText('HelloNameOne').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").frameLocator("iframe[name=\\"one\\"]").getByText("HelloNameOne").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe[name=\\"one\\"]").contentFrame().getByText("HelloNameOne").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe[name=\\"one\\"]").get_by_text("HelloNameOne").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe[name=\\"one\\"]").get_by_text("HelloNameOne").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe[name=\\"one\\"]").content_frame().get_by_text("HelloNameOne").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe[name=\\"one\\"]").GetByText("HelloNameOne").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe[name=\\"one\\"]").ContentFrame().GetByText("HelloNameOne").ClickAsync();`); [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'HelloNameAnonymous'), @@ -183,19 +183,19 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe[n ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').frameLocator('iframe').frameLocator('iframe >> nth=2').getByText('HelloNameAnonymous').click();`); + await page.locator('#frame1').contentFrame().locator('iframe').contentFrame().locator('iframe').nth(2).contentFrame().getByText('HelloNameAnonymous').click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").frameLocator("iframe").frameLocator("iframe >> nth=2").getByText("HelloNameAnonymous").click();`); + page.locator("#frame1").contentFrame().locator("iframe").contentFrame().locator("iframe").nth(2).contentFrame().getByText("HelloNameAnonymous").click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe >> nth=2").get_by_text("HelloNameAnonymous").click()`); + page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").frame_locator("iframe").frame_locator("iframe >> nth=2").get_by_text("HelloNameAnonymous").click()`); + await page.locator("#frame1").content_frame().locator("iframe").content_frame().locator("iframe").nth(2).content_frame().get_by_text("HelloNameAnonymous").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe >> nth=2").GetByText("HelloNameAnonymous").ClickAsync();`); +await page.Locator("#frame1").ContentFrame().Locator("iframe").ContentFrame().Locator("iframe").Nth(2).ContentFrame().GetByText("HelloNameAnonymous").ClickAsync();`); }); test('should generate frame locators with special characters in name attribute', async ({ page, openRecorder, server }) => { @@ -208,22 +208,22 @@ await page.FrameLocator("#frame1").FrameLocator("iframe").FrameLocator("iframe > }); const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('iframe[name="foo"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('iframe[name="foo"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('iframe[name="foo\\\\"]').getByRole('button', { name: 'Click me' }).click();`); + await page.locator('iframe[name="foo\\\\"]').contentFrame().getByRole('button', { name: 'Click me' }).click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("iframe[name=\\"foo\\\\\\"]").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`); + page.locator("iframe[name=\\"foo\\\\\\"]").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Click me")).click()`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("iframe[name=\\"foo\\\\\\"]").get_by_role("button", name="Click me").click()`); + page.locator("iframe[name=\\"foo\\\\\\"]").content_frame().get_by_role("button", name="Click me").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("iframe[name=\\"foo\\\\\\"]").get_by_role("button", name="Click me").click()`); + await page.locator("iframe[name=\\"foo\\\\\\"]").content_frame().get_by_role("button", name="Click me").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("iframe[name=\\"foo\\\\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`); +await page.Locator("iframe[name=\\"foo\\\\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`); }); test('should generate frame locators with title attribute', async ({ page, openRecorder, server }) => { @@ -234,27 +234,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[title="hello world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[title="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('iframe[title="hello world"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('iframe[title="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"iframe[title=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"iframe[title=\\\"hello world\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"iframe[title=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"iframe[title=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("iframe[title=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("iframe[title=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("iframe[title=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("iframe[title=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -266,27 +266,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[name="hello world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[name="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('iframe[name="hello world"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('iframe[name="hello world"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"iframe[name=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"iframe[name=\\\"hello world\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"iframe[name=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"iframe[name=\\\"hello world\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("iframe[name=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("iframe[name=\\\"hello world\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("iframe[name=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("iframe[name=\\\"hello world\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -298,27 +298,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'Click me'), - page.frameLocator('[id="hello-world"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('[id="hello-world"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('#hello-world').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('#hello-world').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"#hello-world\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"#hello-world\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"#hello-world\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"#hello-world\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("#hello-world").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("#hello-world").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("#hello-world").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("#hello-world").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -330,27 +330,27 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") const [sources] = await Promise.all([ recorder.waitForOutput('JavaScript', 'my-testid'), - page.frameLocator('iframe[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click(), + page.locator('iframe[data-testid="my-testid"]').contentFrame().getByRole('button', { name: 'Click me' }).click(), ]); - expect(sources.get('JavaScript')!.text).toContain( - `await page.frameLocator('[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click();` + expect.soft(sources.get('JavaScript')!.text).toContain( + `await page.locator('[data-testid="my-testid"]').contentFrame().getByRole('button', { name: 'Click me' }).click();` ); - expect(sources.get('Java')!.text).toContain( - `page.frameLocator(\"[data-testid=\\\"my-testid\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` + expect.soft(sources.get('Java')!.text).toContain( + `page.locator(\"[data-testid=\\\"my-testid\\\"]\").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();` ); - expect(sources.get('Python')!.text).toContain( - `page.frame_locator(\"[data-testid=\\\"my-testid\\\"]\").get_by_role(\"button\", name=\"Click me\").click()` + expect.soft(sources.get('Python')!.text).toContain( + `page.locator(\"[data-testid=\\\"my-testid\\\"]\").content_frame().get_by_role(\"button\", name=\"Click me\").click()` ); - expect(sources.get('Python Async')!.text).toContain( - `await page.frame_locator("[data-testid=\\\"my-testid\\\"]").get_by_role("button", name="Click me").click()` + expect.soft(sources.get('Python Async')!.text).toContain( + `await page.locator("[data-testid=\\\"my-testid\\\"]").content_frame().get_by_role("button", name="Click me").click()` ); - expect(sources.get('C#')!.text).toContain( - `await page.FrameLocator("[data-testid=\\\"my-testid\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` + expect.soft(sources.get('C#')!.text).toContain( + `await page.Locator("[data-testid=\\\"my-testid\\\"]").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();` ); }); @@ -365,19 +365,19 @@ await page.FrameLocator("iframe[name=\\"foo\\\\\\"]") ]); expect.soft(sources.get('JavaScript')!.text).toContain(` - await page.frameLocator('#frame1').getByRole('button', { name: 'Submit' }).click();`); + await page.locator('#frame1').contentFrame().getByRole('button', { name: 'Submit' }).click();`); expect.soft(sources.get('Java')!.text).toContain(` - page.frameLocator("#frame1").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); + page.locator("#frame1").contentFrame().getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName("Submit")).click();`); expect.soft(sources.get('Python')!.text).toContain(` - page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); + page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); expect.soft(sources.get('Python Async')!.text).toContain(` - await page.frame_locator("#frame1").get_by_role("button", name="Submit").click()`); + await page.locator("#frame1").content_frame().get_by_role("button", name="Submit").click()`); expect.soft(sources.get('C#')!.text).toContain(` -await page.FrameLocator("#frame1").GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); +await page.Locator("#frame1").ContentFrame().GetByRole(AriaRole.Button, new() { Name = "Submit" }).ClickAsync();`); }); test('should generate getByTestId', async ({ page, openRecorder }) => { diff --git a/tests/library/modernizr.spec.ts b/tests/library/modernizr.spec.ts index 9db5b24aa5..2fd4e5c955 100644 --- a/tests/library/modernizr.spec.ts +++ b/tests/library/modernizr.spec.ts @@ -21,7 +21,7 @@ import os from 'os'; async function checkFeatures(name: string, context: any, server: any) { try { const page = await context.newPage(); - await page.goto(server.PREFIX + '/modernizr.html'); + await page.goto(server.PREFIX + '/modernizr/index.html'); const actual = await page.evaluate('window.report'); const expected = JSON.parse(fs.readFileSync(require.resolve(`../assets/modernizr/${name}.json`), 'utf-8')); return { actual, expected }; @@ -30,28 +30,38 @@ async function checkFeatures(name: string, context: any, server: any) { } } -it('safari-14-1', async ({ browser, browserName, platform, server, headless, isMac }) => { +it('Safari Desktop', async ({ browser, browserName, platform, server, headless, isMac }) => { it.skip(browserName !== 'webkit'); it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const context = await browser.newContext({ deviceScaleFactor: 2 }); - const { actual, expected } = await checkFeatures('safari-14-1', context, server); + const { actual, expected } = await checkFeatures('safari-18', context, server); + + expected.pushmanager = false; + expected.devicemotion2 = false; + expected.devicemotion = false; + expected.deviceorientation = false; + expected.deviceorientation3 = false; + + delete expected.webglextensions; + delete actual.webglextensions; + expected.audio = !!expected.audio; + actual.audio = !!actual.audio; + expected.video = !!expected.video; + actual.video = !!actual.video; if (platform === 'linux') { expected.subpixelfont = false; expected.speechrecognition = false; + expected.publickeycredential = false; + expected.mediastream = false; if (headless) expected.todataurljpeg = false; // GHA delete actual.variablefonts; delete expected.variablefonts; - - if (isDocker()) { - delete actual.unicode; - delete expected.unicode; - } } if (platform === 'win32') { @@ -61,30 +71,34 @@ it('safari-14-1', async ({ browser, browserName, platform, server, headless, isM expected.speechrecognition = false; expected.speechsynthesis = false; expected.todataurljpeg = false; - expected.unicode = false; expected.webaudio = false; expected.gamepads = false; expected.input.list = false; + delete expected.datalistelem; + + expected.publickeycredential = false; + expected.mediastream = false; + expected.mediasource = false; + expected.datachannel = false; + expected.inputtypes.color = false; + expected.inputtypes.month = false; + expected.inputtypes.week = false; expected.inputtypes.date = false; expected.inputtypes['datetime-local'] = false; expected.inputtypes.time = false; } - if (isMac && parseInt(os.release(), 10) > 20) - expected.applicationcache = false; - expect(actual).toEqual(expected); }); -it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => { +it('Mobile Safari', async ({ playwright, browser, browserName, platform, isMac, server, headless }) => { it.skip(browserName !== 'webkit'); - it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) < 20, 'WebKit for macOS 10.15 is frozen.'); it.skip(browserName === 'webkit' && platform === 'darwin' && parseInt(os.release(), 10) === 22, 'Modernizr uses WebGL which is not available in macOS-13 - https://bugs.webkit.org/show_bug.cgi?id=278277'); const iPhone = playwright.devices['iPhone 12']; const context = await browser.newContext(iPhone); - const { actual, expected } = await checkFeatures('mobile-safari-14-1', context, server); + const { actual, expected } = await checkFeatures('mobile-safari-18', context, server); { // All platforms. @@ -93,22 +107,28 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.cssvhunit = true; expected.cssvmaxunit = true; expected.overflowscrolling = false; + expected.mediasource = true; + expected.scrolltooptions = false; + + delete expected.webglextensions; + delete actual.webglextensions; + expected.audio = !!expected.audio; + actual.audio = !!actual.audio; + expected.video = !!expected.video; + actual.video = !!actual.video; } if (platform === 'linux') { expected.subpixelfont = false; expected.speechrecognition = false; + expected.publickeycredential = false; + expected.mediastream = false; if (headless) expected.todataurljpeg = false; // GHA delete actual.variablefonts; delete expected.variablefonts; - - if (isDocker()) { - delete actual.unicode; - delete expected.unicode; - } } if (platform === 'win32') { @@ -118,32 +138,25 @@ it('mobile-safari-14-1', async ({ playwright, browser, browserName, platform, is expected.speechrecognition = false; expected.speechsynthesis = false; expected.todataurljpeg = false; - expected.unicode = false; expected.webaudio = false; expected.gamepads = false; expected.input.list = false; + + delete expected.datalistelem; + + expected.publickeycredential = false; + expected.mediastream = false; + expected.mediasource = false; + expected.datachannel = false; + expected.inputtypes.color = false; expected.inputtypes.month = false; expected.inputtypes.week = false; expected.inputtypes.date = false; - expected.inputtypes.time = false; expected.inputtypes['datetime-local'] = false; expected.inputtypes.time = false; } expect(actual).toEqual(expected); }); - -function isDocker() { - try { - fs.statSync('/.dockerenv'); - return true; - } catch { - } - try { - return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker'); - } catch { - } - return false; -} diff --git a/tests/library/permissions.spec.ts b/tests/library/permissions.spec.ts index 592ee80691..1064eaf61f 100644 --- a/tests/library/permissions.spec.ts +++ b/tests/library/permissions.spec.ts @@ -119,12 +119,7 @@ it.describe('permissions', () => { await context.grantPermissions(['geolocation'], { origin: server.EMPTY_PAGE }); expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted']); await context.clearPermissions(); - - // Note: Chromium 110 stopped triggering "onchange" when clearing permissions. - expect(await page.evaluate(() => window['events'])).toEqual( - (browserName === 'chromium' && browserMajorVersion === 110) ? - ['prompt', 'denied', 'granted'] : - ['prompt', 'denied', 'granted', 'prompt']); + expect(await page.evaluate(() => window['events'])).toEqual(['prompt', 'denied', 'granted', 'prompt']); }); it('should isolate permissions between browser contexts', async ({ server, browser }) => { diff --git a/tests/library/role-utils.spec.ts b/tests/library/role-utils.spec.ts index 6c45686f68..a02680ce86 100644 --- a/tests/library/role-utils.spec.ts +++ b/tests/library/role-utils.spec.ts @@ -22,8 +22,8 @@ test.skip(({ mode }) => mode !== 'default'); async function getNameAndRole(page: Page, selector: string) { return await page.$eval(selector, e => { - const name = (window as any).__injectedScript.getElementAccessibleName(e); - const role = (window as any).__injectedScript.getAriaRole(e); + const name = (window as any).__injectedScript.utils.getElementAccessibleName(e); + const role = (window as any).__injectedScript.utils.getAriaRole(e); return { name, role }; }); } @@ -89,7 +89,7 @@ for (let range = 0; range <= ranges.length; range++) { if (!element) throw new Error(`Unable to resolve "${step.selector}"`); const injected = (window as any).__injectedScript; - const received = step.property === 'name' ? injected.getElementAccessibleName(element) : injected.getElementAccessibleDescription(element); + const received = step.property === 'name' ? injected.utils.getElementAccessibleName(element) : injected.utils.getElementAccessibleDescription(element); result.push({ selector: step.selector, expected: step.value, received }); } return result; @@ -152,7 +152,7 @@ test('wpt accname non-manual', async ({ page, asset, server }) => { const injected = (window as any).__injectedScript; const title = element.getAttribute('data-testname'); const expected = element.getAttribute('data-expectedlabel'); - const received = injected.getElementAccessibleName(element); + const received = injected.utils.getElementAccessibleName(element); result.push({ title, expected, received }); } return result; @@ -180,7 +180,7 @@ test('axe-core implicit-role', async ({ page, asset, server }) => { const element = document.querySelector(selector); if (!element) throw new Error(`Unable to resolve "${selector}"`); - return (window as any).__injectedScript.getAriaRole(element); + return (window as any).__injectedScript.utils.getAriaRole(element); }, testCase.target); expect.soft(received, `checking ${JSON.stringify(testCase)}`).toBe(testCase.role); }); @@ -213,7 +213,7 @@ test('axe-core accessible-text', async ({ page, asset, server }) => { const element = injected.querySelector(injected.parseSelector('css=' + selector), document, false); if (!element) throw new Error(`Unable to resolve "${selector}"`); - return injected.getElementAccessibleName(element); + return injected.utils.getElementAccessibleName(element); }); }, targets); expect.soft(received, `checking ${JSON.stringify(testCase)}`).toEqual(expected); diff --git a/tests/page/jshandle-to-string.spec.ts b/tests/page/jshandle-to-string.spec.ts index 930899ae3d..dc0a6695bb 100644 --- a/tests/page/jshandle-to-string.spec.ts +++ b/tests/page/jshandle-to-string.spec.ts @@ -56,7 +56,7 @@ it('should work for promises', async ({ page }) => { expect(bHandle.toString()).toBe('Promise'); }); -it('should work with different subtypes @smoke', async ({ page, browserName, browserMajorVersion }) => { +it('should work with different subtypes @smoke', async ({ page, browserName }) => { expect((await page.evaluateHandle('(function(){})')).toString()).toContain('function'); expect((await page.evaluateHandle('12')).toString()).toBe('12'); expect((await page.evaluateHandle('true')).toString()).toBe('true'); @@ -71,7 +71,7 @@ it('should work with different subtypes @smoke', async ({ page, browserName, bro expect((await page.evaluateHandle('new WeakMap()')).toString()).toBe('WeakMap'); expect((await page.evaluateHandle('new WeakSet()')).toString()).toBe('WeakSet'); expect((await page.evaluateHandle('new Error()')).toString()).toContain('Error'); - expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe((browserName === 'chromium' && browserMajorVersion >= 111) ? 'Proxy(Object)' : 'Proxy'); + expect((await page.evaluateHandle('new Proxy({}, {})')).toString()).toBe((browserName === 'chromium') ? 'Proxy(Object)' : 'Proxy'); }); it('should work with previewable subtypes', async ({ page, browserName }) => { diff --git a/tests/page/page-accessibility.spec.ts b/tests/page/page-accessibility.spec.ts index e6717c5e20..a85d3112bd 100644 --- a/tests/page/page-accessibility.spec.ts +++ b/tests/page/page-accessibility.spec.ts @@ -177,7 +177,7 @@ it('rich text editable fields should have children', async function({ page, brow expect(snapshot.children[0]).toEqual(golden); }); -it('rich text editable fields with role should have children', async function({ page, browserName, browserMajorVersion, browserVersion, isWebView2 }) { +it('rich text editable fields with role should have children', async function({ page, browserName, browserVersion, isWebView2 }) { it.skip(browserName === 'webkit', 'WebKit rich text accessibility is iffy'); it.skip(isWebView2, 'WebView2 is missing a Chromium fix'); @@ -196,7 +196,7 @@ it('rich text editable fields with role should have children', async function({ } : { role: 'textbox', name: '', - multiline: (browserName === 'chromium' && browserMajorVersion >= 92) ? true : undefined, + multiline: (browserName === 'chromium') ? true : undefined, value: 'Edit this image: ', children: (chromiumVersionLessThan(browserVersion, '104.0.1293.1') && browserName === 'chromium') ? [{ role: 'text', diff --git a/tests/page/page-add-init-script.spec.ts b/tests/page/page-add-init-script.spec.ts index 2c8234a550..b2b7782eba 100644 --- a/tests/page/page-add-init-script.spec.ts +++ b/tests/page/page-add-init-script.spec.ts @@ -31,12 +31,6 @@ it('should work with a path', async ({ page, server, asset }) => { expect(await page.evaluate(() => window['result'])).toBe(123); }); -it('should assume CJS module with a path and arg', async ({ page, server, asset }) => { - await page.addInitScript({ path: asset('injectedmodule.js') }, 17); - await page.goto(server.EMPTY_PAGE); - expect(await page.evaluate(() => window['injected'])).toBe(17); -}); - it('should work with content @smoke', async ({ page, server }) => { await page.addInitScript({ content: 'window["injected"] = 123' }); await page.goto(server.PREFIX + '/tamperable.html'); diff --git a/tests/page/page-goto.spec.ts b/tests/page/page-goto.spec.ts index 015fa0e4ef..ccab5abeba 100644 --- a/tests/page/page-goto.spec.ts +++ b/tests/page/page-goto.spec.ts @@ -179,27 +179,6 @@ it('should work with Cross-Origin-Opener-Policy after redirect', async ({ page, expect(firstRequest.url()).toBe(server.PREFIX + '/redirect'); }); -it('should properly cancel Cross-Origin-Opener-Policy navigation', { - annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32107' }, -}, async ({ page, server, browserName, isLinux, headless }) => { - it.fixme(browserName === 'webkit' && isLinux, 'Started failing after https://commits.webkit.org/281488@main'); - it.fixme(browserName === 'chromium' && headless, 'COOP navigation cancels the one that starts later'); - server.setRoute('/empty.html', (req, res) => { - res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); - res.end(); - }); - const requestPromise = page.waitForRequest(server.EMPTY_PAGE); - page.goto(server.EMPTY_PAGE).catch(() => {}); - await new Promise(f => setTimeout(f, 50)); - // Non COOP response. - await page.goto(server.CROSS_PROCESS_PREFIX + '/error.html'); - const req = await requestPromise; - const response = await Promise.race([req.response(), new Promise(f => setTimeout(() => f('timeout'), 5_000))]); - // First navigation request should either receive response or be canceled by the second - // navigation, but never hang unresolved. - expect(response).not.toBe('timeout'); -}); - it('should capture iframe navigation request', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); expect(page.url()).toBe(server.EMPTY_PAGE); @@ -366,7 +345,7 @@ it('should fail when main resources failed to load', async ({ page, browserName, } else if (browserName === 'webkit' && isWindows && mode === 'service2') { expect(error.message).toContain(`proxy handshake error`); } else if (browserName === 'webkit' && isWindows) { - expect(error.message).toContain(`Couldn\'t connect to server`); + expect(error.message).toContain(`Could not connect to server`); } else if (browserName === 'webkit') { if (mode === 'service2') expect(error.message).toContain('Connection refused'); diff --git a/tests/page/page-network-response.spec.ts b/tests/page/page-network-response.spec.ts index ecae86987a..41d7178208 100644 --- a/tests/page/page-network-response.spec.ts +++ b/tests/page/page-network-response.spec.ts @@ -270,14 +270,14 @@ it('should behave the same way for headers and allHeaders', async ({ page, serve expect(allHeaders['name-b']).toEqual('v4'); }); -it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, browserMajorVersion, mode }) => { +it('should provide a Response with a file URL', async ({ page, asset, isAndroid, isElectron, isWindows, browserName, mode }) => { it.skip(isAndroid, 'No files on Android'); it.skip(browserName === 'firefox', 'Firefox does return null for file:// URLs'); it.skip(mode.startsWith('service')); const fileurl = url.pathToFileURL(asset('frames/two-frames.html')).href; const response = await page.goto(fileurl); - if (isElectron || (browserName === 'chromium' && browserMajorVersion >= 99) || (browserName === 'webkit' && isWindows)) + if (isElectron || (browserName === 'chromium') || (browserName === 'webkit' && isWindows)) expect(response.status()).toBe(200); else expect(response.status()).toBe(0); diff --git a/tests/page/page-route.spec.ts b/tests/page/page-route.spec.ts index 911521018e..9bdb41f8d6 100644 --- a/tests/page/page-route.spec.ts +++ b/tests/page/page-route.spec.ts @@ -512,7 +512,7 @@ it('should work with badly encoded server', async ({ page, server }) => { expect(response.status()).toBe(200); }); -it('should work with encoded server - 2', async ({ page, server, browserName, browserMajorVersion }) => { +it('should work with encoded server - 2', async ({ page, server, browserName }) => { // The requestWillBeSent will report URL as-is, whereas interception will // report encoded URL for stylesheet. @see crbug.com/759388 const requests = []; @@ -522,7 +522,7 @@ it('should work with encoded server - 2', async ({ page, server, browserName, br }); const response = await page.goto(`data:text/html,`); expect(response).toBe(null); - if (browserName === 'firefox' && browserMajorVersion >= 97) + if (browserName === 'firefox') expect(requests.length).toBe(2); // Firefox DevTools report to navigations in this case as well. else expect(requests.length).toBe(1); diff --git a/tests/page/wheel.spec.ts b/tests/page/wheel.spec.ts index c8c634e16d..7a1fccc2aa 100644 --- a/tests/page/wheel.spec.ts +++ b/tests/page/wheel.spec.ts @@ -22,8 +22,8 @@ it.skip(({ isAndroid }) => { let ignoreDelta = false; -it.beforeAll(async ({ browserMajorVersion, browserName, isElectron, platform }) => { - if (((browserName === 'chromium' && browserMajorVersion >= 102) || isElectron) && platform === 'darwin') { +it.beforeAll(async ({ browserName, isElectron, platform }) => { + if (((browserName === 'chromium') || isElectron) && platform === 'darwin') { // Chromium reports deltaX/deltaY scaled by host device scale factor. // https://bugs.chromium.org/p/chromium/issues/detail?id=1324819 // https://github.com/microsoft/playwright/issues/7362 diff --git a/tests/playwright-test/expect-poll.spec.ts b/tests/playwright-test/expect-poll.spec.ts index e740fd5abe..344fdccdee 100644 --- a/tests/playwright-test/expect-poll.spec.ts +++ b/tests/playwright-test/expect-poll.spec.ts @@ -44,7 +44,10 @@ test('should poll predicate', async ({ runInlineTest }) => { test('should compile', async ({ runTSC }) => { const result = await runTSC({ 'a.spec.ts': ` - import { test, expect } from '@playwright/test'; + import { test, expect as baseExpect } from '@playwright/test'; + const expect = baseExpect.extend({ + toBeWithinRange() { return { message: () => "is within range", pass: true }; }, + }) test('should poll sync predicate', async ({ page }) => { let i = 0; test.expect.poll(() => ++i).toBe(3); @@ -57,6 +60,7 @@ test('should compile', async ({ runTSC }) => { return ++i; }).toBe(3); test.expect.poll(() => Promise.resolve(++i)).toBe(3); + expect.poll(() => Promise.resolve(++i)).toBeWithinRange(); // @ts-expect-error await test.expect.poll(() => page.locator('foo')).toBeEnabled(); @@ -172,7 +176,9 @@ test('should support .not predicate', async ({ runInlineTest }) => { test('should support custom matchers', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.spec.ts': ` - expect.extend({ + import { test, expect as baseExpect } from '@playwright/test'; + + const expect = baseExpect.extend({ toBeWithinRange(received, floor, ceiling) { const pass = received >= floor && received <= ceiling; if (pass) { @@ -191,10 +197,9 @@ test('should support custom matchers', async ({ runInlineTest }) => { }, }); - import { test, expect } from '@playwright/test'; test('should poll', async () => { let i = 0; - await test.expect.poll(() => ++i).toBeWithinRange(3, Number.MAX_VALUE); + await expect.poll(() => ++i).toBeWithinRange(3, Number.MAX_VALUE); }); ` }); @@ -232,3 +237,29 @@ test('should show intermediate result for poll that spills over test time', asyn expect(result.output).toContain('Expected: 2'); expect(result.output).toContain('Received: 3'); }); + +test('should propagate promise rejections', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32256' } }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('should fail', async () => { + await expect.poll(() => Promise.reject('some error')).toBe({ foo: 'baz' }); + }); + ` + }); + + expect(result.output).toContain('some error'); +}); + +test('should propagate string exception from async arrow function', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright/issues/32256' } }, async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('should fail', async () => { + await expect.poll(async () => { throw 'some error' }).toBe({ foo: 'baz' }); + }); + ` + }); + + expect(result.output).toContain('some error'); +}); \ No newline at end of file diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts index 768cd28fc2..1f5da44a84 100644 --- a/tests/playwright-test/reporter-html.spec.ts +++ b/tests/playwright-test/reporter-html.spec.ts @@ -802,6 +802,32 @@ for (const useIntermediateMergeReport of [false] as const) { await expect(page.locator('.attachment-body')).toHaveText(['foo', '{"foo":1}', 'utf16 encoded']); }); + test('should have link for opening HTML attachments in new tab', async ({ runInlineTest, page, showReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + import { test, expect } from '@playwright/test'; + test('passing', async ({ page }, testInfo) => { + testInfo.attach('axe-report.html', { + contentType: 'text/html', + body: '

Axe Report

', + }); + }); + `, + }, { reporter: 'dot,html' }, { PLAYWRIGHT_HTML_OPEN: 'never' }); + expect(result.exitCode).toBe(0); + + await showReport(); + await page.getByText('passing', { exact: true }).click(); + + const [newTab] = await Promise.all([ + page.waitForEvent('popup'), + page.getByText('axe-report.html', { exact: true }).click(), + ]); + + await expect(newTab).toHaveURL(/^blob:/); + await expect(newTab.getByText('Axe Report')).toBeVisible(); + }); + test('should use file-browser friendly extensions for buffer attachments based on contentType', async ({ runInlineTest, showReport, page }, testInfo) => { const result = await runInlineTest({ 'a.test.js': ` diff --git a/tests/playwright-test/test-tag.spec.ts b/tests/playwright-test/test-tag.spec.ts index 9487e31ea3..0587cfe7a8 100644 --- a/tests/playwright-test/test-tag.spec.ts +++ b/tests/playwright-test/test-tag.spec.ts @@ -147,6 +147,18 @@ test('should enforce @ symbol', async ({ runInlineTest }) => { expect(result.output).toContain(`Error: Tag must start with "@" symbol, got "foo" instead.`); }); +test('types should enforce @ symbol', async ({ runTSC }) => { + const result = await runTSC({ + 'stdio.spec.ts': ` + import { test, expect } from '@playwright/test'; + test('test1', { tag: 'foo' }, () => { + }); + ` + }); + expect(result.exitCode).toBe(2); + expect(result.output).toContain('error TS2322: Type \'"foo"\' is not assignable to type \'`@${string}` | `@${string}`[] | undefined'); +}); + test('should be included in testInfo', async ({ runInlineTest }, testInfo) => { const result = await runInlineTest({ 'a.test.ts': ` diff --git a/tests/playwright-test/ui-mode-test-network-tab.spec.ts b/tests/playwright-test/ui-mode-test-network-tab.spec.ts index 45d77aa528..8fc7a4828e 100644 --- a/tests/playwright-test/ui-mode-test-network-tab.spec.ts +++ b/tests/playwright-test/ui-mode-test-network-tab.spec.ts @@ -93,3 +93,45 @@ test('should filter network requests by url', async ({ runUITest, server }) => { await expect(networkItems).toHaveCount(1); await expect(networkItems.getByText('font.woff2')).toBeVisible(); }); + +test('should format JSON request body', async ({ runUITest, server }) => { + const { page } = await runUITest({ + 'network-tab.test.ts': ` + import { test, expect } from '@playwright/test'; + test('network tab test', async ({ page }) => { + await page.goto('${server.PREFIX}/network-tab/network.html'); + }); + `, + }); + + await page.getByText('network tab test').dblclick(); + await page.getByText('Network', { exact: true }).click(); + + await page.getByText('post-data-1').click(); + + await expect(page.locator('.CodeMirror-code .CodeMirror-line').allInnerTexts()).resolves.toEqual([ + '{', + ' "data": {', + ' "key": "value",', + ' "array": [', + ' "value-1",', + ' "value-2"', + ' ]', + ' }', + '}', + ]); + + await page.getByText('post-data-2').click(); + + await expect(page.locator('.CodeMirror-code .CodeMirror-line').allInnerTexts()).resolves.toEqual([ + '{', + ' "data": {', + ' "key": "value",', + ' "array": [', + ' "value-1",', + ' "value-2"', + ' ]', + ' }', + '}', + ]); +}); diff --git a/tests/playwright-test/ui-mode-test-setup.spec.ts b/tests/playwright-test/ui-mode-test-setup.spec.ts index e8809ddad9..cd5503427d 100644 --- a/tests/playwright-test/ui-mode-test-setup.spec.ts +++ b/tests/playwright-test/ui-mode-test-setup.spec.ts @@ -19,7 +19,7 @@ import path from 'path'; test.describe.configure({ mode: 'parallel', retries }); -test('should run global setup and teardown', async ({ runUITest }) => { +test('should run global setup and teardown', async ({ runUITest }, testInfo) => { const { page, testProcess } = await runUITest({ 'playwright.config.ts': ` import { defineConfig } from '@playwright/test'; @@ -29,26 +29,36 @@ test('should run global setup and teardown', async ({ runUITest }) => { }); `, 'globalSetup.ts': ` - export default () => console.log('\\n%%from-global-setup'); + import { basename } from "node:path"; + export default (config) => { + console.log('\\n%%from-global-setup'); + console.log("setupOutputDir: " + basename(config.projects[0].outputDir)); + }; `, 'globalTeardown.ts': ` - export default () => console.log('\\n%%from-global-teardown'); + export default (config) => { + console.log('\\n%%from-global-teardown'); + console.log('%%' + JSON.stringify(config)); + }; `, 'a.test.js': ` import { test, expect } from '@playwright/test'; test('should work', async ({}) => {}); ` - }); + }, undefined, { additionalArgs: ['--output=foo'] }); await page.getByTitle('Run all').click(); await expect(page.getByTestId('status-line')).toHaveText('1/1 passed (100%)'); await page.getByTitle('Toggle output').click(); - await expect(page.getByTestId('output')).toContainText('from-global-setup'); + const output = page.getByTestId('output'); + await expect(output).toContainText('from-global-setup'); + await expect(output).toContainText('setupOutputDir: foo'); await page.close(); - await expect.poll(() => testProcess.outputLines()).toEqual([ - 'from-global-teardown', - ]); + await expect.poll(() => testProcess.outputLines()).toContain('from-global-teardown'); + + const teardownConfig = JSON.parse(testProcess.outputLines()[1]); + expect(teardownConfig.projects[0].outputDir).toEqual(testInfo.outputPath('foo')); }); test('should teardown on sigint', async ({ runUITest, nodeVersion }) => { diff --git a/tests/playwright-test/ui-mode-test-source.spec.ts b/tests/playwright-test/ui-mode-test-source.spec.ts index 5d2973a8cf..1dfcd7b87e 100644 --- a/tests/playwright-test/ui-mode-test-source.spec.ts +++ b/tests/playwright-test/ui-mode-test-source.spec.ts @@ -128,3 +128,31 @@ test('should show syntax errors in file', async ({ runUITest }) => { /Missing semicolon./ ]); }); + +test('should load error (dupe tests) indicator on sources', async ({ runUITest }) => { + const { page } = await runUITest({ + 'a.test.ts': ` + import { test } from '@playwright/test'; + test('first', () => {}); + test('first', () => {}); + `, + }); + await expect.poll(dumpTestTree(page)).toBe(` + ▼ ◯ a.test.ts + ◯ first + `); + + await page.getByTestId('test-tree').getByText('a.test.ts').click(); + await expect(page.getByText('Source1')).toBeVisible(); + + await expect( + page.locator('.CodeMirror .source-line-running'), + ).toHaveText(`4 test('first', () => {});`); + + await expect( + page.locator('.CodeMirror-linewidget') + ).toHaveText([ + '                              ', + /Error: duplicate test title "first", first declared in a.test.ts:3/ + ]); +}); diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 5c108d7b25..a5d6eeb0b9 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -70,8 +70,10 @@ type TestDetailsAnnotation = { description?: string; }; +type TestDetailsTag = `@${string}`; + export type TestDetails = { - tag?: string | string[]; + tag?: TestDetailsTag | TestDetailsTag[]; annotation?: TestDetailsAnnotation | TestDetailsAnnotation[]; } @@ -403,15 +405,17 @@ type MakeMatchers = { rejects: MakeMatchers, any, ExtendedMatchers>; } & IfAny, SpecificMatchers & ToUserMatcherObject>; +type PollMatchers = { + /** + * If you know how to test something, `.not` lets you test its opposite. + */ + not: PollMatchers; +} & BaseMatchers & ToUserMatcherObject; + export type Expect = { (actual: T, messageOrOptions?: string | { message?: string }): MakeMatchers; soft: (actual: T, messageOrOptions?: string | { message?: string }) => MakeMatchers; - poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => BaseMatchers, T> & { - /** - * If you know how to test something, `.not` lets you test its opposite. - */ - not: BaseMatchers, T>; - }; + poll: (actual: () => T | Promise, messageOrOptions?: string | { message?: string, timeout?: number, intervals?: number[] }) => PollMatchers, T, ExtendedMatchers>; extend MatcherReturnType | Promise>>(matchers: MoreMatchers): Expect; configure: (configuration: { message?: string, diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 578602959a..e679bbb9cb 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -64,7 +64,15 @@ export interface Page { exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Frame { @@ -106,12 +114,28 @@ export interface BrowserContext { addInitScript(script: PageFunction | { path?: string, content?: string }, arg?: Arg): Promise; removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Browser { removeAllListeners(type?: string): this; - removeAllListeners(type: string | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise; + removeAllListeners(type: string | undefined, options: { + /** + * Specifies whether to wait for already running listeners and what to do if they throw errors: + * - `'default'` - do not wait for current listener calls (if any) to finish, if the listener throws, it may result in unhandled error + * - `'wait'` - wait for current listener calls (if any) to finish + * - `'ignoreErrors'` - do not wait for current listener calls (if any) to finish, all errors thrown by the listeners after removal are silently caught + */ + behavior?: 'wait'|'ignoreErrors'|'default' + }): Promise; } export interface Worker {