mirror of
https://github.com/withastro/astro.git
synced 2025-01-06 22:10:10 -05:00
feat(audits): Handle mutations (#10268)
* feat(audits): Handle mutations * chore: changeset * nit: add comments
This commit is contained in:
parent
0204b7de37
commit
2013e70bce
6 changed files with 224 additions and 3 deletions
5
.changeset/two-ads-bathe.md
Normal file
5
.changeset/two-ads-bathe.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"astro": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Adds support for page mutations to the audits in the dev toolbar. Astro will now rerun the audits whenever elements are added or deleted from the page.
|
|
@ -44,6 +44,115 @@ test.describe('Dev Toolbar - Audits', () => {
|
||||||
await appButton.click();
|
await appButton.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('can handle mutations', async ({ page, astro }) => {
|
||||||
|
await page.goto(astro.resolveUrl('/audits-mutations'));
|
||||||
|
|
||||||
|
const toolbar = page.locator('astro-dev-toolbar');
|
||||||
|
const appButton = toolbar.locator('button[data-app-id="astro:audit"]');
|
||||||
|
await appButton.click();
|
||||||
|
|
||||||
|
const auditCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="astro:audit"]');
|
||||||
|
const auditHighlights = auditCanvas.locator('astro-dev-toolbar-highlight');
|
||||||
|
await expect(auditHighlights).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.click('body');
|
||||||
|
|
||||||
|
const badButton = page.locator('#bad-button');
|
||||||
|
|
||||||
|
let consolePromise = page.waitForEvent('console');
|
||||||
|
await badButton.click();
|
||||||
|
await consolePromise;
|
||||||
|
|
||||||
|
await appButton.click();
|
||||||
|
await expect(auditHighlights).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('multiple changes only result in one audit update', async ({ page, astro }) => {
|
||||||
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'astro:dev-toolbar:settings',
|
||||||
|
JSON.stringify({
|
||||||
|
verbose: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(astro.resolveUrl('/audits-mutations'));
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
logs.push(msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
const badButton = page.locator('#bad-button');
|
||||||
|
|
||||||
|
let consolePromise = page.waitForEvent('console', (msg) =>
|
||||||
|
msg.text().includes('Rerunning audit lints')
|
||||||
|
);
|
||||||
|
await badButton.click({ clickCount: 5 });
|
||||||
|
await consolePromise;
|
||||||
|
|
||||||
|
await page.click('body');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
logs.filter((log) => log.includes('Rerunning audit lints because the DOM has been updated'))
|
||||||
|
.length === 1
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handle mutations properly during view transitions', async ({ page, astro }) => {
|
||||||
|
await page.goto(astro.resolveUrl('/'));
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'astro:dev-toolbar:settings',
|
||||||
|
JSON.stringify({
|
||||||
|
verbose: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(astro.resolveUrl('/audits-mutations'));
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
logs.push(msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkToOtherPage = page.locator('#link-to-2');
|
||||||
|
let consolePromise = page.waitForEvent('console');
|
||||||
|
await linkToOtherPage.click();
|
||||||
|
await consolePromise;
|
||||||
|
|
||||||
|
const toolbar = page.locator('astro-dev-toolbar');
|
||||||
|
const appButton = toolbar.locator('button[data-app-id="astro:audit"]');
|
||||||
|
|
||||||
|
await appButton.click();
|
||||||
|
|
||||||
|
const auditCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="astro:audit"]');
|
||||||
|
const auditHighlights = auditCanvas.locator('astro-dev-toolbar-highlight');
|
||||||
|
await expect(auditHighlights).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.click('body');
|
||||||
|
|
||||||
|
const badButton = page.locator('#bad-button-2');
|
||||||
|
|
||||||
|
consolePromise = page.waitForEvent('console');
|
||||||
|
await badButton.click();
|
||||||
|
await consolePromise;
|
||||||
|
|
||||||
|
await appButton.click();
|
||||||
|
await expect(auditHighlights).toHaveCount(2);
|
||||||
|
|
||||||
|
// Make sure we only reran audits once
|
||||||
|
expect(
|
||||||
|
logs.filter((log) => log.includes('Rerunning audit lints because the DOM has been updated'))
|
||||||
|
.length === 1
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('does not warn for non-interactive element', async ({ page, astro }) => {
|
test('does not warn for non-interactive element', async ({ page, astro }) => {
|
||||||
await page.goto(astro.resolveUrl('/a11y-exceptions'));
|
await page.goto(astro.resolveUrl('/a11y-exceptions'));
|
||||||
|
|
||||||
|
|
|
@ -272,7 +272,6 @@ test.describe('Dev Toolbar', () => {
|
||||||
await appButton.click();
|
await appButton.click();
|
||||||
|
|
||||||
const myAppCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="my-plugin"]');
|
const myAppCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="my-plugin"]');
|
||||||
console.log(await myAppCanvas.innerHTML());
|
|
||||||
const myAppWindow = myAppCanvas.locator('astro-dev-toolbar-window');
|
const myAppWindow = myAppCanvas.locator('astro-dev-toolbar-window');
|
||||||
await expect(myAppWindow).toHaveCount(1);
|
await expect(myAppWindow).toHaveCount(1);
|
||||||
await expect(myAppWindow).toBeVisible();
|
await expect(myAppWindow).toBeVisible();
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
import Layout from "../layout/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<button id="bad-button-2">Click me to add an image that is missing an alt!</button>
|
||||||
|
<a id="link-to-1" href="/audits-mutations">Go to Mutations 1</a>
|
||||||
|
|
||||||
|
<br /><br /><br />
|
||||||
|
<img src="" width="100" height="100" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
const badButton = document.getElementById('bad-button-2');
|
||||||
|
if (!badButton) return;
|
||||||
|
|
||||||
|
badButton.addEventListener('click', clickHandler);
|
||||||
|
|
||||||
|
function clickHandler() {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.width = 100;
|
||||||
|
img.height = 100;
|
||||||
|
|
||||||
|
document.body.appendChild(img);
|
||||||
|
console.log("Image added to the page")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</Layout>
|
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
import Layout from "../layout/Layout.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout>
|
||||||
|
<button id="bad-button">Click me to add an image that is missing an alt!</button>
|
||||||
|
<a id="link-to-2" href="/audits-mutations-2">Go to Mutations 2</a>
|
||||||
|
|
||||||
|
<img src="" width="100" height="100" />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
const badButton = document.getElementById('bad-button');
|
||||||
|
if (!badButton) return;
|
||||||
|
|
||||||
|
badButton.addEventListener('click', clickHandler);
|
||||||
|
|
||||||
|
function clickHandler() {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.width = 100;
|
||||||
|
img.height = 100;
|
||||||
|
|
||||||
|
document.body.appendChild(img);
|
||||||
|
console.log("Image added to the page")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
</Layout>
|
|
@ -10,6 +10,7 @@ import {
|
||||||
import { closeOnOutsideClick, createWindowElement } from '../utils/window.js';
|
import { closeOnOutsideClick, createWindowElement } from '../utils/window.js';
|
||||||
import { a11y } from './a11y.js';
|
import { a11y } from './a11y.js';
|
||||||
import { perf } from './perf.js';
|
import { perf } from './perf.js';
|
||||||
|
import { settings } from '../../settings.js';
|
||||||
|
|
||||||
const icon =
|
const icon =
|
||||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 1 20 16"><path fill="#fff" d="M.6 2A1.1 1.1 0 0 1 1.7.9h16.6a1.1 1.1 0 1 1 0 2.2H1.6A1.1 1.1 0 0 1 .8 2Zm1.1 7.1h6a1.1 1.1 0 0 0 0-2.2h-6a1.1 1.1 0 0 0 0 2.2ZM9.3 13H1.8a1.1 1.1 0 1 0 0 2.2h7.5a1.1 1.1 0 1 0 0-2.2Zm11.3 1.9a1.1 1.1 0 0 1-1.5 0l-1.7-1.7a4.1 4.1 0 1 1 1.6-1.6l1.6 1.7a1.1 1.1 0 0 1 0 1.6Zm-5.3-3.4a1.9 1.9 0 1 0 0-3.8 1.9 1.9 0 0 0 0 3.8Z"/></svg>';
|
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 1 20 16"><path fill="#fff" d="M.6 2A1.1 1.1 0 0 1 1.7.9h16.6a1.1 1.1 0 1 1 0 2.2H1.6A1.1 1.1 0 0 1 .8 2Zm1.1 7.1h6a1.1 1.1 0 0 0 0-2.2h-6a1.1 1.1 0 0 0 0 2.2ZM9.3 13H1.8a1.1 1.1 0 1 0 0 2.2h7.5a1.1 1.1 0 1 0 0-2.2Zm11.3 1.9a1.1 1.1 0 0 1-1.5 0l-1.7-1.7a4.1 4.1 0 1 1 1.6-1.6l1.6 1.7a1.1 1.1 0 0 1 0 1.6Zm-5.3-3.4a1.9 1.9 0 1 0 0-3.8 1.9 1.9 0 0 0 0 3.8Z"/></svg>';
|
||||||
|
@ -69,8 +70,51 @@ export default {
|
||||||
|
|
||||||
await lint();
|
await lint();
|
||||||
|
|
||||||
document.addEventListener('astro:after-swap', async () => lint());
|
let mutationDebounce: ReturnType<typeof setTimeout>;
|
||||||
document.addEventListener('astro:page-load', async () => refreshLintPositions);
|
const observer = new MutationObserver(() => {
|
||||||
|
// We don't want to rerun the audit lints on every single mutation, so we'll debounce it.
|
||||||
|
if (mutationDebounce) {
|
||||||
|
clearTimeout(mutationDebounce);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutationDebounce = setTimeout(() => {
|
||||||
|
settings.logger.verboseLog('Rerunning audit lints because the DOM has been updated.');
|
||||||
|
|
||||||
|
// Even though we're ready to run the lints, we'll wait for the next idle period to do so, as it is less likely
|
||||||
|
// to interfere with any other work the browser is doing post-mutation. For instance, the page or the user might
|
||||||
|
// be interacting with the newly added elements, or the browser might be doing some work (layout, paint, etc.)
|
||||||
|
if ('requestIdleCallback' in window) {
|
||||||
|
window.requestIdleCallback(
|
||||||
|
async () => {
|
||||||
|
lint();
|
||||||
|
},
|
||||||
|
{ timeout: 300 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback for old versions of Safari, we'll assume that things are less likely to be busy after 150ms.
|
||||||
|
setTimeout(() => {
|
||||||
|
lint();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
setupObserver();
|
||||||
|
|
||||||
|
document.addEventListener('astro:before-preparation', () => {
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
document.addEventListener('astro:after-swap', async () => {
|
||||||
|
lint();
|
||||||
|
});
|
||||||
|
document.addEventListener('astro:page-load', async () => {
|
||||||
|
refreshLintPositions();
|
||||||
|
|
||||||
|
// HACK: View transitions add a route announcer after this event, so we need to wait for it to be added
|
||||||
|
setTimeout(() => {
|
||||||
|
setupObserver();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
closeOnOutsideClick(eventTarget);
|
closeOnOutsideClick(eventTarget);
|
||||||
|
|
||||||
|
@ -380,5 +424,12 @@ export default {
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupObserver() {
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
} satisfies DevToolbarApp;
|
} satisfies DevToolbarApp;
|
||||||
|
|
Loading…
Reference in a new issue