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 API

+ +
+ +
+

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 @@ + + + + + +

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 @@ + + + + + + +

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.'); +});