-
Notifications
You must be signed in to change notification settings - Fork 28
add favicon feature #1561
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
add favicon feature #1561
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
27bbb43
add favicon feature
shakyShane 121c852
linting
shakyShane a1937bf
send the first set immediately
shakyShane c028fc5
sample instead of debouncing
shakyShane a1e0abb
send full href
shakyShane f48db81
only in top
shakyShane 73cf52f
full href value
shakyShane f1dfefe
ignore messages from favicons feature
shakyShane 3a20cad
comments/logs
shakyShane 483c472
support new additions
shakyShane 348ba4b
don't run on iOS
shakyShane 6d5e37d
remove pause
shakyShane 2c94e26
break from all loops
shakyShane eb50be8
add missing test for when the feature is disabled as a whole
shakyShane File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
--- | ||
title: Favicon Monitor | ||
--- | ||
|
||
# Favicon Monitor | ||
|
||
Reports the presence of favicons on page-load, and optionally when they change. | ||
|
||
## Notifications | ||
|
||
### `faviconFound` | ||
- {@link "Favicon Messages".FaviconFoundNotification} | ||
- Sent on page load, sends {@link "Favicon Messages".FaviconFound} | ||
|
||
**Example** | ||
|
||
```json | ||
{ | ||
"favicons": [ | ||
{ | ||
"href": "favicon.png", | ||
"rel": "stylesheet" | ||
} | ||
], | ||
"documentUrl": "https://example.com" | ||
} | ||
``` | ||
|
||
## Remote Config | ||
|
||
## Enabled (default) | ||
{@includeCode ../integration-test/test-pages/favicon/config/favicon-enabled.json} | ||
|
||
### Disable the monitor only. | ||
|
||
To only receive the initial payload and nothing more (to mimic the old behavior), | ||
you can set `monitor: false` in the remote config, and it will not install the Mutation Observer. | ||
|
||
{@includeCode ../integration-test/test-pages/favicon/config/favicon-monitor-disabled.json} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { test, expect } from '@playwright/test'; | ||
import { ResultsCollector } from './page-objects/results-collector.js'; | ||
|
||
const HTML = '/favicon/index.html'; | ||
const CONFIG = './integration-test/test-pages/favicon/config/favicon-enabled.json'; | ||
|
||
test('favicon feature absent', async ({ page, baseURL }, testInfo) => { | ||
const CONFIG = './integration-test/test-pages/favicon/config/favicon-absent.json'; | ||
const favicon = ResultsCollector.create(page, testInfo.project.use); | ||
await favicon.load(HTML, CONFIG); | ||
|
||
// ensure first favicon item was sent | ||
const messages = await favicon.waitForMessage('faviconFound', 1); | ||
const url = new URL('/favicon/favicon.png', baseURL); | ||
|
||
expect(messages[0].payload.params).toStrictEqual({ | ||
favicons: [{ href: url.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}); | ||
}); | ||
|
||
test('favicon + monitor', async ({ page, baseURL }, testInfo) => { | ||
const favicon = ResultsCollector.create(page, testInfo.project.use); | ||
await favicon.load(HTML, CONFIG); | ||
|
||
// ensure first favicon item was sent | ||
await favicon.waitForMessage('faviconFound', 1); | ||
|
||
// now update it | ||
await page.getByRole('button', { name: 'Set override' }).click(); | ||
|
||
// wait for the second message | ||
const messages = await favicon.waitForMessage('faviconFound', 2); | ||
|
||
const url1 = new URL('/favicon/favicon.png', baseURL); | ||
const url2 = new URL('/favicon/new_favicon.png', baseURL); | ||
|
||
expect(messages[0].payload.params).toStrictEqual({ | ||
favicons: [{ href: url1.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}); | ||
|
||
expect(messages[1].payload.params).toStrictEqual({ | ||
favicons: [{ href: url2.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}); | ||
}); | ||
|
||
test('favicon + monitor (many updates)', async ({ page, baseURL }, testInfo) => { | ||
const favicon = ResultsCollector.create(page, testInfo.project.use); | ||
await page.clock.install(); | ||
await favicon.load(HTML, CONFIG); | ||
|
||
// ensure first favicon item was sent | ||
await favicon.waitForMessage('faviconFound', 1); | ||
|
||
// now update it | ||
await page.getByRole('button', { name: 'Set many overrides' }).click(); | ||
await page.clock.fastForward(20); | ||
|
||
const messages = await favicon.outgoingMessages(); | ||
expect(messages).toHaveLength(1); | ||
|
||
await page.clock.fastForward(60); | ||
await page.clock.fastForward(100); | ||
|
||
{ | ||
const messages = await favicon.outgoingMessages(); | ||
expect(messages).toHaveLength(3); | ||
} | ||
|
||
{ | ||
const url1 = new URL('/favicon/favicon.png', baseURL); | ||
const url2 = new URL('/favicon/new_favicon.png?count=0', baseURL); | ||
const url3 = new URL('/favicon/new_favicon.png?count=1', baseURL); | ||
|
||
const messages = await favicon.outgoingMessages(); | ||
expect(messages.map((x) => /** @type {{params: any}} */ (x.payload).params)).toStrictEqual([ | ||
{ | ||
favicons: [{ href: url1.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}, | ||
{ | ||
favicons: [{ href: url2.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}, | ||
{ | ||
favicons: [{ href: url3.href, rel: 'shortcut icon' }], | ||
documentUrl: 'http://localhost:3220/favicon/index.html', | ||
}, | ||
]); | ||
} | ||
}); | ||
|
||
test('favicon + monitor disabled', async ({ page }, testInfo) => { | ||
const CONFIG = './integration-test/test-pages/favicon/config/favicon-monitor-disabled.json'; | ||
const favicon = ResultsCollector.create(page, testInfo.project.use); | ||
|
||
await page.clock.install(); | ||
|
||
await favicon.load(HTML, CONFIG); | ||
|
||
// ensure first favicon item was sent | ||
await favicon.waitForMessage('faviconFound', 1); | ||
|
||
// now update it | ||
await page.getByRole('button', { name: 'Set override' }).click(); | ||
|
||
await expect(page.locator('link')).toHaveAttribute('href', './new_favicon.png'); | ||
|
||
// account for the debounce | ||
await page.clock.fastForward(200); | ||
|
||
// ensure only 1 message was still sent (ie: the monitor is disabled) | ||
const messages = await favicon.outgoingMessages(); | ||
expect(messages).toHaveLength(1); | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
injected/integration-test/test-pages/favicon/config/favicon-absent.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"features": { | ||
|
||
}, | ||
"unprotectedTemporary": [] | ||
} |
12 changes: 12 additions & 0 deletions
12
injected/integration-test/test-pages/favicon/config/favicon-enabled.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"features": { | ||
"favicon": { | ||
"state": "enabled", | ||
"exceptions": [], | ||
"settings": { | ||
"monitor": true | ||
} | ||
} | ||
}, | ||
"unprotectedTemporary": [] | ||
} |
12 changes: 12 additions & 0 deletions
12
injected/integration-test/test-pages/favicon/config/favicon-monitor-disabled.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"features": { | ||
"favicon": { | ||
"state": "enabled", | ||
"exceptions": [], | ||
"settings": { | ||
"monitor": false | ||
} | ||
} | ||
}, | ||
"unprotectedTemporary": [] | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<title>Document</title> | ||
<link rel="shortcut icon" href="./favicon.png"> | ||
</head> | ||
<body> | ||
<button onclick="setOverride()">Set override</button> | ||
<button onclick="setManyOverrides()">Set many overrides</button> | ||
<script> | ||
function setOverride() { | ||
document.querySelector("link[rel='shortcut icon']").href = "./new_favicon.png"; | ||
} | ||
async function setManyOverrides() { | ||
const elem = document.querySelector("link[rel='shortcut icon']"); | ||
for (let i = 0; i < 100; i++) { | ||
await new Promise(resolve => { | ||
setTimeout(resolve, 40); | ||
}) | ||
const path = `./new_favicon.png?count=${i}` | ||
elem.href = path | ||
} | ||
} | ||
</script> | ||
</body> | ||
</html> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions
4
injected/integration-test/test-pages/message-bridge/config/message-bridge-disabled.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import ContentFeature from '../content-feature.js'; | ||
import { isBeingFramed } from '../utils.js'; | ||
|
||
export class Favicon extends ContentFeature { | ||
init() { | ||
/** | ||
* This feature never operates in a frame | ||
*/ | ||
if (isBeingFramed()) return; | ||
|
||
window.addEventListener('DOMContentLoaded', () => { | ||
// send once, immediately | ||
this.send(); | ||
|
||
// then optionally watch for changes | ||
this.monitorChanges(); | ||
}); | ||
} | ||
|
||
monitorChanges() { | ||
// if there was an explicit opt-out, do nothing | ||
// this allows the remote config to be absent for this feature | ||
if (this.getFeatureSetting('monitor') === false) return; | ||
|
||
let trailing; | ||
let lastEmitTime = performance.now(); | ||
const interval = 50; | ||
|
||
monitor(() => { | ||
clearTimeout(trailing); | ||
const currentTime = performance.now(); | ||
const delta = currentTime - lastEmitTime; | ||
if (delta >= interval) { | ||
this.send(); | ||
} else { | ||
trailing = setTimeout(() => { | ||
this.send(); | ||
}, 50); | ||
} | ||
lastEmitTime = currentTime; | ||
}); | ||
shakyShane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
send() { | ||
const favicons = getFaviconList(); | ||
this.notify('faviconFound', { favicons, documentUrl: document.URL }); | ||
} | ||
} | ||
|
||
export default Favicon; | ||
|
||
/** | ||
* @param {()=>void} changeObservedCallback | ||
* @param {Element} [target] | ||
*/ | ||
function monitor(changeObservedCallback, target = document.head) { | ||
shakyShane marked this conversation as resolved.
Show resolved
Hide resolved
shakyShane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const observer = new MutationObserver((mutations) => { | ||
for (const mutation of mutations) { | ||
if (mutation.type === 'attributes' && mutation.target instanceof HTMLLinkElement) { | ||
changeObservedCallback(); | ||
break; | ||
} | ||
} | ||
}); | ||
observer.observe(target, { attributeFilter: ['rel', 'href'], attributes: true, subtree: true }); | ||
shakyShane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* @returns {import('../types/favicon.js').FaviconAttrs[]} | ||
*/ | ||
function getFaviconList() { | ||
const selectors = [ | ||
"link[href][rel='favicon']", | ||
"link[href][rel*='icon']", | ||
"link[href][rel='apple-touch-icon']", | ||
"link[href][rel='apple-touch-icon-precomposed']", | ||
]; | ||
const elements = document.head.querySelectorAll(selectors.join(',')); | ||
return Array.from(elements).map((/** @type {HTMLLinkElement} */ link) => { | ||
const href = link.href || ''; | ||
shakyShane marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const rel = link.getAttribute('rel') || ''; | ||
return { href, rel }; | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-07/schema#", | ||
"type": "object", | ||
"title": "FaviconFound", | ||
"required": ["favicons", "documentUrl"], | ||
"properties": { | ||
"favicons": { | ||
"type": "array", | ||
"items": { | ||
"type": "object", | ||
"required": ["rel", "href"], | ||
"title": "Favicon Attrs", | ||
"properties": { | ||
"href": { | ||
"type": "string" | ||
}, | ||
"rel": { | ||
"type": "string" | ||
} | ||
} | ||
} | ||
}, | ||
"documentUrl": { | ||
"type": "string" | ||
} | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.