summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Erika <3019731+Princesseuh@users.noreply.github.com> 2024-03-08 11:56:23 +0100
committerGravatar GitHub <noreply@github.com> 2024-03-08 11:56:23 +0100
commit2013e70bce16366781cc12e52823bb257fe460c0 (patch)
treed93edd8076fa3c5d65fc09cf47bd6623638493f8
parent0204b7de37bf626e1b97175b605adbf91d885386 (diff)
downloadastro-2013e70bce16366781cc12e52823bb257fe460c0.tar.gz
astro-2013e70bce16366781cc12e52823bb257fe460c0.tar.zst
astro-2013e70bce16366781cc12e52823bb257fe460c0.zip
feat(audits): Handle mutations (#10268)
* feat(audits): Handle mutations * chore: changeset * nit: add comments
-rw-r--r--.changeset/two-ads-bathe.md5
-rw-r--r--packages/astro/e2e/dev-toolbar-audits.test.js109
-rw-r--r--packages/astro/e2e/dev-toolbar.test.js1
-rw-r--r--packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations-2.astro29
-rw-r--r--packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations.astro28
-rw-r--r--packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts55
6 files changed, 224 insertions, 3 deletions
diff --git a/.changeset/two-ads-bathe.md b/.changeset/two-ads-bathe.md
new file mode 100644
index 000000000..beb330146
--- /dev/null
+++ b/.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.
diff --git a/packages/astro/e2e/dev-toolbar-audits.test.js b/packages/astro/e2e/dev-toolbar-audits.test.js
index 2195aa9f9..cbb89ab72 100644
--- a/packages/astro/e2e/dev-toolbar-audits.test.js
+++ b/packages/astro/e2e/dev-toolbar-audits.test.js
@@ -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'));
diff --git a/packages/astro/e2e/dev-toolbar.test.js b/packages/astro/e2e/dev-toolbar.test.js
index b2e6242b4..49472fbb3 100644
--- a/packages/astro/e2e/dev-toolbar.test.js
+++ b/packages/astro/e2e/dev-toolbar.test.js
@@ -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();
diff --git a/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations-2.astro b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations-2.astro
new file mode 100644
index 000000000..e1c95e3e4
--- /dev/null
+++ b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations-2.astro
@@ -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>
diff --git a/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations.astro b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations.astro
new file mode 100644
index 000000000..1c4950a2f
--- /dev/null
+++ b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/audits-mutations.astro
@@ -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>
diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts
index e07e6c6ac..1e7e3009c 100644
--- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts
+++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/index.ts
@@ -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;