diff options
author | 2024-03-08 11:56:23 +0100 | |
---|---|---|
committer | 2024-03-08 11:56:23 +0100 | |
commit | 2013e70bce16366781cc12e52823bb257fe460c0 (patch) | |
tree | d93edd8076fa3c5d65fc09cf47bd6623638493f8 | |
parent | 0204b7de37bf626e1b97175b605adbf91d885386 (diff) | |
download | astro-2013e70bce16366781cc12e52823bb257fe460c0.tar.gz astro-2013e70bce16366781cc12e52823bb257fe460c0.tar.zst astro-2013e70bce16366781cc12e52823bb257fe460c0.zip |
feat(audits): Handle mutations (#10268)
* feat(audits): Handle mutations
* chore: changeset
* nit: add comments
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, '"') .replace(/'/g, '''); } + + function setupObserver() { + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } }, } satisfies DevToolbarApp; |