docs: add mock API project examples (#12244)

This commit is contained in:
Yury Semikhatsky 2022-02-28 16:16:05 -08:00 committed by GitHub
parent d744a87aee
commit a6fecb5d4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 675 additions and 0 deletions

View File

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

View File

@ -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).

View File

@ -0,0 +1,51 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Battery Status API - Demo</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta http-equiv="cleartype" content="on">
<meta name="HandheldFriendly" content="True">
<link rel="stylesheet" href="src/styles.css">
</head>
<body>
<header class="demo-header">
<h1 class="demo-title">Battery Status API</h1>
<p class="not-support" hidden>Your browser doesn't support the Battery Status API :(</p>
</header>
<article class="battery-card">
<h1 class="battery-title">Battery Status</h1>
<div class="battery-box">
<strong class="battery-percentage"></strong>
<i class="battery"></i>
</div>
<dl class="battery-info">
<dt>Power Source</dt>
<dd class="battery-status">---</dd>
<dt>Level percentage</dt>
<dd class="battery-level">---</dd>
<dt>Fully charged in</dt>
<dd class="battery-fully">---</dd>
<dt>Remaining time</dt>
<dd class="battery-remaining">---</dd>
</dl>
</article>
<footer>
<a href="https://github.com/pazguille/demo-battery-api" id="github-ribbon"><img width="149" height="149" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a>
by <a href="http://pazguille.me/">Guille Paz</a> with <span class="heart"></span>
<iframe id="github-button" src="https://ghbtns.com/github-btn.html?user=pazguille&amp;repo=demo-battery-api&amp;type=watch&amp;count=true" allowtransparency="true" frameborder="0" scrolling="0" width="152" height="30"></iframe>
</footer>
<script src="src/index.js" async></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

View File

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

View File

@ -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) {}

View File

@ -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"
}
}

View File

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

View File

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

View File

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

View File

@ -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'
]);
});

View File

@ -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"
}
}

View File

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

View File

@ -0,0 +1,15 @@
<head>
<script>
async function loadFile() {
const [fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
document.getElementById('contents').textContent = contents
}
</script>
</head>
<body>
<button onclick="loadFile()">Open File</button>
<p>Pick a text file and its contents will be shown below</p>
<textarea id="contents" placeholder="File contents"></textarea>
</body>

View File

@ -0,0 +1,35 @@
<head>
<style>
div.directory::before {
content: '📁';
}
</style>
<script>
async function loadDir() {
const handle = await window.showDirectoryPicker();
for await (const entry of handle.values())
await listEntry(entry, 0);
}
// List filesystem entry recursively
async function listEntry(e, offset) {
offset += 2;
printEntry(e.name, offset, e.kind);
if (e.kind !== 'directory')
return;
for await (const entry of e.values())
await listEntry(entry, offset);
}
function printEntry(text, offset, kind) {
const div = document.createElement('div');
div.style.paddingLeft = offset * 10;
div.classList.add(kind);
div.textContent = text;
document.getElementById('dir').appendChild(div);
}
</script>
</head>
<body>
<button id="ls-directory" onclick="loadDir()">Open directory</button>
<p>Directory contents:</p>
<div id="dir"></div>
</body>

View File

@ -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'
]);
});

View File

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