diff --git a/examples/mock-battery/demo-battery-api/LICENSE b/examples/mock-battery/demo-battery-api/LICENSE
new file mode 100644
index 0000000000..585630c32d
--- /dev/null
+++ b/examples/mock-battery/demo-battery-api/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Guille Paz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/examples/mock-battery/demo-battery-api/README.md b/examples/mock-battery/demo-battery-api/README.md
new file mode 100644
index 0000000000..5a18a73cb4
--- /dev/null
+++ b/examples/mock-battery/demo-battery-api/README.md
@@ -0,0 +1,25 @@
+# Battery Status API
+
+> Battery Status API Demo
+
+## Demo
+http://pazguille.github.io/demo-battery-api/
+
+## Support
+- Chrome 38+
+- Chrome for Android
+- Firefox 31+
+
+## Specs
+http://www.w3.org/TR/battery-status
+
+## Maintained by
+- Guille Paz (Frontender & Web standards lover)
+- E-mail: [guille87paz@gmail.com](mailto:guille87paz@gmail.com)
+- Twitter: [@pazguille](http://twitter.com/pazguille)
+- Web: [http://pazguille.me](http://pazguille.me)
+
+## License
+Licensed under the MIT license.
+
+Copyright (c) 2014 [@pazguille](http://twitter.com/pazguille).
diff --git a/examples/mock-battery/demo-battery-api/index.html b/examples/mock-battery/demo-battery-api/index.html
new file mode 100644
index 0000000000..37225351b7
--- /dev/null
+++ b/examples/mock-battery/demo-battery-api/index.html
@@ -0,0 +1,51 @@
+
+
+
+
+ Battery Status API - Demo
+
+
+
+
+
+
+
+
+
+
+
+ Battery Status
+
+
+
+
+
+
+
+ Power Source
+ ---
+
+ Level percentage
+ ---
+
+ Fully charged in
+ ---
+
+ Remaining time
+ ---
+
+
+
+
+
+
+
+
+
diff --git a/examples/mock-battery/demo-battery-api/src/bolt.png b/examples/mock-battery/demo-battery-api/src/bolt.png
new file mode 100644
index 0000000000..b9d6504862
Binary files /dev/null and b/examples/mock-battery/demo-battery-api/src/bolt.png differ
diff --git a/examples/mock-battery/demo-battery-api/src/index.js b/examples/mock-battery/demo-battery-api/src/index.js
new file mode 100644
index 0000000000..9ee0876447
--- /dev/null
+++ b/examples/mock-battery/demo-battery-api/src/index.js
@@ -0,0 +1,71 @@
+(function() {
+ 'use strict';
+
+ var battery;
+
+ function toTime(sec) {
+ sec = parseInt(sec, 10);
+
+ var hours = Math.floor(sec / 3600),
+ minutes = Math.floor((sec - (hours * 3600)) / 60),
+ seconds = sec - (hours * 3600) - (minutes * 60);
+
+ if (hours < 10) { hours = '0' + hours; }
+ if (minutes < 10) { minutes = '0' + minutes; }
+ if (seconds < 10) { seconds = '0' + seconds; }
+
+ return hours + ':' + minutes;
+ }
+
+ function readBattery(b) {
+ battery = b || battery;
+
+ var percentage = parseFloat((battery.level * 100).toFixed(2)) + '%',
+ fully,
+ remaining;
+
+ if (battery.charging && battery.chargingTime === Infinity) {
+ fully = 'Calculating...';
+ } else if (battery.chargingTime !== Infinity) {
+ fully = toTime(battery.chargingTime);
+ } else {
+ fully = '---';
+ }
+
+ if (!battery.charging && battery.dischargingTime === Infinity) {
+ remaining = 'Calculating...';
+ } else if (battery.dischargingTime !== Infinity) {
+ remaining = toTime(battery.dischargingTime);
+ } else {
+ remaining = '---';
+ }
+
+ document.styleSheets[0].insertRule('.battery:before{width:' + percentage + '}', 0);
+ document.querySelector('.battery-percentage').innerHTML = percentage;
+ document.querySelector('.battery-status').innerHTML = battery.charging ? 'Adapter' : 'Battery';
+ document.querySelector('.battery-level').innerHTML = percentage;
+ document.querySelector('.battery-fully').innerHTML = fully;
+ document.querySelector('.battery-remaining').innerHTML = remaining;
+
+ }
+
+ if (navigator.battery) {
+ readBattery(navigator.battery);
+
+ } else if (navigator.getBattery) {
+ navigator.getBattery().then(readBattery);
+
+ } else {
+ document.querySelector('.not-support').removeAttribute('hidden');
+ }
+
+ window.onload = function () {
+ battery.addEventListener('chargingchange', function() {
+ readBattery();
+ });
+
+ battery.addEventListener("levelchange", function() {
+ readBattery();
+ });
+ };
+}());
diff --git a/examples/mock-battery/demo-battery-api/src/styles.css b/examples/mock-battery/demo-battery-api/src/styles.css
new file mode 100644
index 0000000000..15a50f1a67
--- /dev/null
+++ b/examples/mock-battery/demo-battery-api/src/styles.css
@@ -0,0 +1,156 @@
+/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}
+/**
+ * Mobile First
+ */
+body {
+ font: 100%/1.4em "Helvetica Neue", "Helvetica", "Arial", sans-serif;
+ margin: 0 auto;
+ padding: 0 0.625em;
+ color: #444;
+ -webkit-text-size-adjust: none;
+}
+
+/**
+ * Small Screens
+ */
+
+.demo-header {
+ margin-bottom: 80px;
+ text-align: center;
+}
+
+.demo-title {
+ font-size: 4em;
+ line-height: 1em;
+ text-align: center;
+}
+
+.battery-card {
+ font-family: "Helvetica", Arial, sans-serif;
+ display: block;
+ width: 300px;
+ overflow: hidden;
+ border: 1px solid #D5D5D5;
+ border-radius: 6px;
+ font-weight: 100;
+ margin: 0 auto;
+}
+
+.battery-title {
+ background: #4c4c4c url('bolt.png') no-repeat 95% 15px;
+ color: #fff;
+ font-size: .9em;
+ line-height: 50px;
+ padding: 0 15px;
+ font-weight: 100;
+ margin: 0;
+}
+
+.battery-percentage {
+ font-size: 2.5em;
+ line-height: 50px;
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 10px;
+}
+
+.battery-box {
+ margin: 0 auto;
+ padding: 50px 0;
+ text-align: center;
+ border-bottom: 1px solid #D5D5D5;
+}
+
+.battery {
+ display: inline-block;
+ position: relative;
+ border: 4px solid #000;
+ width: 85px;
+ height: 40px;
+ border-radius: 4px;
+ vertical-align: middle;
+}
+
+.battery:before {
+ content: '';
+ display: block;
+ box-sizing: border-box;
+ background: #000;
+ height: 40px;
+ position: absolute;
+ border: 1px solid #fff;
+}
+
+.battery:after {
+ content: '';
+ display: block;
+ background: #000;
+ width: 6px;
+ height: 16px;
+ position: absolute;
+ top: 50%;
+ right: -11px;
+ margin-top: -8px;
+ border-radius: 0 4px 4px 0;
+}
+
+.battery-info {
+ font-size: 12px;
+ margin: 0 auto;
+ padding: 15px 45px;
+ overflow: hidden;
+}
+
+.battery-info dd {
+ float: right;
+ margin-top: -22px;
+ text-align: left;
+ width: 35%;
+}
+
+footer {
+ margin: 70px auto 0;
+ text-align: center;
+}
+
+.heart {
+ font-style: normal;
+ font-weight: 500;
+ color: #c0392b;
+ text-decoration: none;
+}
+
+#github-button {
+ display: block;
+ margin: 30px auto 0;
+ position: relative;
+ left: 40px;
+}
+
+#github-ribbon {
+ display: inline-block;
+ position: fixed;
+ top: 0;
+ right: 0;
+ z-index: 100;
+ border: 0;
+ width: 149px;
+ height: 149px;
+}
+
+.github-buttons {
+ text-align: center;
+ margin: 1em 0;
+}
+
+
+
+/**
+ * Medium Screens
+ */
+@media all and (min-width:40em) {}
+
+/**
+ * Large Screens
+ */
+@media all and (min-width: 54em) {}
\ No newline at end of file
diff --git a/examples/mock-battery/package.json b/examples/mock-battery/package.json
new file mode 100644
index 0000000000..dc78f0ca89
--- /dev/null
+++ b/examples/mock-battery/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "mock-battery",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "playwright test",
+ "start": "http-server -c-1 -p 9900 demo-battery-api"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.19.1",
+ "http-server": "^14.1.0"
+ }
+}
diff --git a/examples/mock-battery/playwright.config.js b/examples/mock-battery/playwright.config.js
new file mode 100644
index 0000000000..9372a46c53
--- /dev/null
+++ b/examples/mock-battery/playwright.config.js
@@ -0,0 +1,16 @@
+// @ts-check
+const path = require('path')
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ * @type{import('@playwright/test').PlaywrightTestConfig}
+ */
+const config = {
+ webServer: {
+ port: 9900,
+ command: 'npm run start',
+ },
+ // Test directory
+ testDir: path.join(__dirname, 'tests'),
+};
+module.exports = config;
diff --git a/examples/mock-battery/tests/show-battery-status.spec.js b/examples/mock-battery/tests/show-battery-status.spec.js
new file mode 100644
index 0000000000..ccf97ff188
--- /dev/null
+++ b/examples/mock-battery/tests/show-battery-status.spec.js
@@ -0,0 +1,26 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+test.beforeEach(async ({ page }) => {
+ await page.addInitScript(() => {
+ const mockBattery = {
+ level: 0.90,
+ charging: true,
+ chargingTime: 1800, // seconds
+ dischargingTime: Infinity,
+ addEventListener: () => { }
+ };
+ // application tries navigator.battery first
+ // so we delete this method
+ delete window.navigator.battery;
+ // Override the method to always return mock battery info.
+ window.navigator.getBattery = async () => mockBattery;
+ });
+});
+
+test('show battery status', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('.battery-percentage')).toHaveText('90%');
+ await expect(page.locator('.battery-status')).toHaveText('Adapter');
+ await expect(page.locator('.battery-fully')).toHaveText('00:30');
+})
diff --git a/examples/mock-battery/tests/update-battery-status.spec.js b/examples/mock-battery/tests/update-battery-status.spec.js
new file mode 100644
index 0000000000..73d319a4b4
--- /dev/null
+++ b/examples/mock-battery/tests/update-battery-status.spec.js
@@ -0,0 +1,81 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+let log = [];
+
+test.beforeEach(async ({page}) => {
+ log = [];
+ // Expose function for pushing messages to the Node.js script.
+ await page.exposeFunction('logCall', msg => log.push(msg));
+
+ await page.addInitScript(() => {
+ // for these tests, return the same mock battery status
+ class BatteryMock {
+ level = 0.10;
+ charging = false;
+ chargingTime = 1800;
+ dischargingTime = Infinity;
+ _chargingListeners = [];
+ _levelListeners = [];
+ addEventListener(eventName, listener) {
+ logCall(`addEventListener:${eventName}`);
+ if (eventName === 'chargingchange')
+ this._chargingListeners.push(listener);
+ if (eventName === 'levelchange')
+ this._levelListeners.push(listener);
+ }
+ _setLevel(value) {
+ this.level = value;
+ this._levelListeners.forEach(cb => cb());
+ }
+ _setCharging(value) {
+ this.charging = value;
+ this._chargingListeners.forEach(cb => cb());
+ }
+ };
+ const mockBattery = new BatteryMock();
+ // Override the method to always return mock battery info.
+ window.navigator.getBattery = async () => {
+ logCall('getBattery');
+ return mockBattery;
+ };
+ // Save the mock object on window for easier access.
+ window.mockBattery = mockBattery;
+
+ // application tries navigator.battery first
+ // so we delete this method
+ delete window.navigator.battery;
+ });
+});
+
+test('should update UI when battery status changes', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('.battery-percentage')).toHaveText('10%');
+
+ // Update level to 27.5%
+ await page.evaluate(() => window.mockBattery._setLevel(0.275));
+ await expect(page.locator('.battery-percentage')).toHaveText('27.5%');
+ await expect(page.locator('.battery-status')).toHaveText('Battery');
+
+ // Emulate connected adapter
+ await page.evaluate(() => window.mockBattery._setCharging(true));
+ await expect(page.locator('.battery-status')).toHaveText('Adapter');
+ await expect(page.locator('.battery-fully')).toHaveText('00:30');
+});
+
+
+test('verify API calls', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('.battery-percentage')).toHaveText('10%');
+
+ // Ensure expected method calls were made.
+ expect(log).toEqual([
+ 'getBattery',
+ 'addEventListener:chargingchange',
+ 'addEventListener:levelchange'
+ ]);
+ log = []; // reset the log
+
+ await page.evaluate(() => window.mockBattery._setLevel(0.275));
+ expect(log).toEqual([]); // getBattery is not called, cached version is used.
+});
diff --git a/examples/mock-battery/tests/verify-calls.spec.js b/examples/mock-battery/tests/verify-calls.spec.js
new file mode 100644
index 0000000000..032ff4b37f
--- /dev/null
+++ b/examples/mock-battery/tests/verify-calls.spec.js
@@ -0,0 +1,39 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+let log = [];
+
+test.beforeEach(async ({page}) => {
+ log = [];
+ // Expose function for pushing messages to the Node.js script.
+ await page.exposeFunction('logCall', msg => log.push(msg));
+ await page.addInitScript(() => {
+ const mockBattery = {
+ level: 0.75,
+ charging: true,
+ chargingTime: 1800, // seconds
+ dischargingTime: Infinity,
+ addEventListener: (name, cb) => logCall(`addEventListener:${name}`)
+ };
+ // Override the method to always return mock battery info.
+ window.navigator.getBattery = async () => {
+ logCall('getBattery');
+ return mockBattery;
+ };
+ // application tries navigator.battery first
+ // so we delete this method
+ delete window.navigator.battery;
+ });
+})
+
+test('verify battery calls', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('.battery-percentage')).toHaveText('75%');
+
+ // Ensure expected method calls were made.
+ expect(log).toEqual([
+ 'getBattery',
+ 'addEventListener:chargingchange',
+ 'addEventListener:levelchange'
+ ]);
+});
diff --git a/examples/mock-filesystem/package.json b/examples/mock-filesystem/package.json
new file mode 100644
index 0000000000..333f434d58
--- /dev/null
+++ b/examples/mock-filesystem/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "mock-filesystem",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "playwright test",
+ "start": "http-server -c-1 -p 9900 src/"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@playwright/test": "^1.19.1",
+ "http-server": "^14.1.0"
+ }
+}
diff --git a/examples/mock-filesystem/playwright.config.js b/examples/mock-filesystem/playwright.config.js
new file mode 100644
index 0000000000..9372a46c53
--- /dev/null
+++ b/examples/mock-filesystem/playwright.config.js
@@ -0,0 +1,16 @@
+// @ts-check
+const path = require('path')
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ * @type{import('@playwright/test').PlaywrightTestConfig}
+ */
+const config = {
+ webServer: {
+ port: 9900,
+ command: 'npm run start',
+ },
+ // Test directory
+ testDir: path.join(__dirname, 'tests'),
+};
+module.exports = config;
diff --git a/examples/mock-filesystem/src/file-picker.html b/examples/mock-filesystem/src/file-picker.html
new file mode 100644
index 0000000000..060ede7b95
--- /dev/null
+++ b/examples/mock-filesystem/src/file-picker.html
@@ -0,0 +1,15 @@
+
+
+
+
+ Open File
+ Pick a text file and its contents will be shown below
+
+
\ No newline at end of file
diff --git a/examples/mock-filesystem/src/ls-dir.html b/examples/mock-filesystem/src/ls-dir.html
new file mode 100644
index 0000000000..8bb1d7a9fa
--- /dev/null
+++ b/examples/mock-filesystem/src/ls-dir.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Open directory
+ Directory contents:
+
+
\ No newline at end of file
diff --git a/examples/mock-filesystem/tests/directory-reader.spec.js b/examples/mock-filesystem/tests/directory-reader.spec.js
new file mode 100644
index 0000000000..8933344aab
--- /dev/null
+++ b/examples/mock-filesystem/tests/directory-reader.spec.js
@@ -0,0 +1,64 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+test.beforeEach(async ({page}) => {
+ await page.addInitScript(() => {
+ class FileSystemHandleMock {
+ constructor({name, children}) {
+ this.name = name;
+ children ??= [];
+ this.kind = children.length ? 'directory' : 'file';
+ this._children = children;
+ }
+
+ values() {
+ // Wrap children data in the same mock.
+ return this._children.map(c => new FileSystemHandleMock(c));
+ }
+ }
+ // Create mock directory
+ const mockDir = new FileSystemHandleMock({
+ name: 'root',
+ children: [
+ {
+ name: 'file1',
+ },
+ {
+ name: 'dir1',
+ children: [
+ {
+ name: 'file2',
+ },
+ {
+ name: 'file3',
+ }
+ ]
+ },
+ {
+ name: 'dir2',
+ children: [
+ {
+ name: 'file4',
+ },
+ {
+ name: 'file5',
+ }
+ ]
+ }
+ ]
+ });
+ // Make the picker return mock directory
+ window.showDirectoryPicker = async () => mockDir;
+ });
+});
+
+test('should display directory tree', async ({ page }) => {
+ await page.goto('/ls-dir.html');
+ await page.locator('button', { hasText: 'Open directory' }).click();
+ // Check that the displayed entries match mock directory.
+ await expect(page.locator('#dir')).toContainText([
+ 'file1',
+ 'dir1', 'file2', 'file3',
+ 'dir2', 'file4', 'file5'
+ ]);
+});
diff --git a/examples/mock-filesystem/tests/file-reader.spec.js b/examples/mock-filesystem/tests/file-reader.spec.js
new file mode 100644
index 0000000000..2a3e0dd770
--- /dev/null
+++ b/examples/mock-filesystem/tests/file-reader.spec.js
@@ -0,0 +1,24 @@
+// @ts-check
+const { test, expect } = require('@playwright/test');
+
+test.beforeEach(async ({page}) => {
+ await page.addInitScript(() => {
+ class FileSystemFileHandleMock {
+ constructor(file) {
+ this._file = file;
+ }
+
+ async getFile() {
+ return this._file;
+ }
+ }
+ window.showOpenFilePicker = async () => [new FileSystemFileHandleMock(new File(['Test content.'], "foo.txt"))];
+ });
+});
+
+test('show file picker with mock class', async ({ page }) => {
+ await page.goto('/file-picker.html');
+ await page.locator('button', { hasText: 'Open File' }).click();
+ // Check that the content of the mock file has been loaded.
+ await expect(page.locator('textarea')).toHaveValue('Test content.');
+});