0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

feat(audits): Handle mutations (#10268)

* feat(audits): Handle mutations

* chore: changeset

* nit: add comments
This commit is contained in:
Erika 2024-03-08 11:56:23 +01:00 committed by GitHub
parent 0204b7de37
commit 2013e70bce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 224 additions and 3 deletions

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

View file

@ -44,6 +44,115 @@ test.describe('Dev Toolbar - Audits', () => {
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 }) => {
await page.goto(astro.resolveUrl('/a11y-exceptions'));

View file

@ -272,7 +272,6 @@ test.describe('Dev Toolbar', () => {
await appButton.click();
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');
await expect(myAppWindow).toHaveCount(1);
await expect(myAppWindow).toBeVisible();

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import {
import { closeOnOutsideClick, createWindowElement } from '../utils/window.js';
import { a11y } from './a11y.js';
import { perf } from './perf.js';
import { settings } from '../../settings.js';
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>';
@ -69,8 +70,51 @@ export default {
await lint();
document.addEventListener('astro:after-swap', async () => lint());
document.addEventListener('astro:page-load', async () => refreshLintPositions);
let mutationDebounce: ReturnType<typeof setTimeout>;
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);
@ -380,5 +424,12 @@ export default {
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function setupObserver() {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
},
} satisfies DevToolbarApp;