Skip to content

Commit

Permalink
feat(audits): Handle mutations (#10268)
Browse files Browse the repository at this point in the history
* feat(audits): Handle mutations

* chore: changeset

* nit: add comments
  • Loading branch information
Princesseuh committed Mar 8, 2024
1 parent 0204b7d commit 2013e70
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-ads-bathe.md
@@ -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.
109 changes: 109 additions & 0 deletions packages/astro/e2e/dev-toolbar-audits.test.js
Expand Up @@ -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'));

Expand Down
1 change: 0 additions & 1 deletion packages/astro/e2e/dev-toolbar.test.js
Expand Up @@ -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();
Expand Down
@@ -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>
55 changes: 53 additions & 2 deletions packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts
Expand Up @@ -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>';
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -380,5 +424,12 @@ export default {
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

function setupObserver() {
observer.observe(document.body, {
childList: true,
subtree: true,
});
}
},
} satisfies DevToolbarApp;

0 comments on commit 2013e70

Please sign in to comment.