Some UI bugs are hard to explain because the important evidence is not the final screen. The evidence is the change: a field becomes disabled, a validation message appears, a button label changes, a modal is injected, or a row disappears after an action. Screenshots help, but they often miss the exact DOM signal that changed between the tester action and the bug report.
This tutorial shows a practical way to build a small Manifest V3 Chrome extension that captures DOM change evidence for QA bug reports. The goal is not to replace Playwright, Selenium, accessibility tools, or DevTools. The goal is to give manual testers, SDETs, and automation engineers a lightweight evidence helper that records observable page changes, page URL metadata, and tester notes after a deliberate user action.
What This Extension Should Capture
The queue focus for this article is Chrome extension DOM change evidence. The useful QA angle is simple: when a tester starts recording, interacts with the application, and then stops recording, the extension should produce a compact evidence bundle that can be pasted into Jira, Azure DevOps, GitHub Issues, or a bug report template.
A practical first version should capture:
- Page URL and document title.
- Viewport size at the time of capture.
- Timestamp for each observed change.
- Change type such as added nodes, removed nodes, or attribute changes.
- A short selector-like summary for the changed element.
- Visible text snippets when safe and useful.
- Tester notes that explain the action performed and expected result.
Keep the scope intentionally narrow. A Chrome extension content script can inspect the page DOM, but it should not claim access to private framework state, server-side state, browser accessibility tree internals, or every visual change. For example, canvas rendering, CSS-only animations, and shadow DOM details may need extra handling. Treat this helper as evidence support, not as a complete testing oracle.
Official Source Notes Used for This Workflow
This workflow is based on current Chrome for Developers extension documentation and primary browser API documentation reviewed on 2026-07-04. Chrome extension documentation describes manifest.json as the required root file for extension metadata and permissions. Chrome content scripts can run JavaScript in web pages, activeTab grants temporary access to the active tab after a user gesture, the scripting API can inject functions or files into a target tab, messaging supports JSON-serializable communication between extension contexts, storage persists JSON-serializable extension state asynchronously, and the side panel API can host a persistent extension UI beside the inspected page. MDN documents MutationObserver as the Web API for watching DOM tree changes.
Reference URLs used for the article:
- Chrome Extensions get started
- activeTab permission
- Content scripts
- chrome.scripting API
- Extension messaging
- chrome.storage API
- chrome.sidePanel API
- MutationObserver on MDN
Step 1: Create the Manifest
Create a folder named qa-dom-change-evidence. Add this manifest.json file. It uses a toolbar action, a service worker, the activeTab permission, the scripting API, and extension storage.
{
"manifest_version": 3,
"name": "QA DOM Change Evidence",
"version": "1.0.0",
"description": "Capture tester-triggered DOM change evidence for QA bug reports.",
"permissions": ["activeTab", "scripting", "storage"],
"action": {
"default_title": "Capture DOM evidence",
"default_popup": "popup.html"
},
"background": {
"service_worker": "service-worker.js"
}
}
This keeps permissions modest. activeTab is a good fit for a tester-clicked capture flow because the tester explicitly chooses the page and starts the extension. If your team later needs always-on capture across specific internal domains, review host permissions carefully and explain why they are needed.
Step 2: Build a Small Popup UI
The popup lets a tester start recording, stop recording, add notes, and copy the evidence. Start with a minimal UI.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>QA DOM Evidence</title>
<style>
body { font: 13px system-ui, sans-serif; width: 340px; margin: 12px; }
button, textarea { width: 100%; margin-top: 8px; }
textarea { min-height: 80px; }
pre { white-space: pre-wrap; max-height: 240px; overflow: auto; }
</style>
</head>
<body>
<button id="start">Start capture</button>
<button id="stop">Stop capture</button>
<textarea id="notes" placeholder="Tester notes: action, expected result, actual result"></textarea>
<button id="copy">Copy bug evidence</button>
<pre id="output"></pre>
<script src="popup.js"></script>
</body>
</html>
A side panel is often better for a mature version because it stays visible while the tester works through the scenario. For a first tutorial, the popup keeps setup simple and screenshot-friendly.
Step 3: Inject a DOM Observer
The content-side code uses MutationObserver to watch for added nodes, removed nodes, and selected attribute changes. Keep the evidence compact so the bug report stays readable.
function describeElement(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) return null;
const element = node;
const tag = element.tagName.toLowerCase();
const id = element.id ? `#${element.id}` : "";
const classes = Array.from(element.classList || []).slice(0, 3).map((name) => `.${name}`).join("");
const role = element.getAttribute("role");
const label = element.getAttribute("aria-label") || element.getAttribute("name");
const text = (element.innerText || element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120);
return {
selectorHint: `${tag}${id}${classes}`,
role: role || undefined,
label: label || undefined,
text: text || undefined
};
}
window.__qaDomEvidence = {
startedAt: new Date().toISOString(),
page: {
url: location.href,
title: document.title,
viewport: `${window.innerWidth}x${window.innerHeight}`
},
changes: []
};
window.__qaDomObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const entry = {
time: new Date().toISOString(),
type: mutation.type,
target: describeElement(mutation.target),
added: Array.from(mutation.addedNodes || []).map(describeElement).filter(Boolean).slice(0, 5),
removed: Array.from(mutation.removedNodes || []).map(describeElement).filter(Boolean).slice(0, 5),
attributeName: mutation.attributeName || undefined
};
if (entry.added.length || entry.removed.length || entry.attributeName) {
window.__qaDomEvidence.changes.push(entry);
window.__qaDomEvidence.changes = window.__qaDomEvidence.changes.slice(-50);
}
}
});
window.__qaDomObserver.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["class", "hidden", "disabled", "aria-expanded", "aria-invalid", "aria-busy"]
});
This example intentionally limits stored changes to the latest 50 entries. That prevents a noisy page from producing an unusable report. You can tune the limit for your product, but do not store unlimited DOM evidence.
Step 4: Control Capture from the Popup
The popup can inject the observer, request the captured evidence, combine it with tester notes, and copy it. This example uses the scripting API to execute small functions in the active tab.
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
return tab;
}
async function runInTab(func) {
const tab = await getActiveTab();
const [result] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func });
return result?.result;
}
document.getElementById("start").addEventListener("click", async () => {
const tab = await getActiveTab();
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ["observer.js"] });
document.getElementById("output").textContent = "DOM capture started. Reproduce the issue now.";
});
document.getElementById("stop").addEventListener("click", async () => {
const evidence = await runInTab(() => {
window.__qaDomObserver?.disconnect();
return window.__qaDomEvidence || null;
});
const notes = document.getElementById("notes").value.trim();
const report = { ...evidence, testerNotes: notes };
await chrome.storage.local.set({ lastDomEvidence: report });
document.getElementById("output").textContent = JSON.stringify(report, null, 2);
});
document.getElementById("copy").addEventListener("click", async () => {
const { lastDomEvidence } = await chrome.storage.local.get("lastDomEvidence");
const text = JSON.stringify(lastDomEvidence || {}, null, 2);
await navigator.clipboard.writeText(text);
document.getElementById("output").textContent = "Copied evidence to clipboard.";
});
Save the observer code as observer.js and the popup logic as popup.js. Add an empty service-worker.js if you do not need background behavior yet.
Step 5: Use It in a QA Workflow
Install the unpacked extension from chrome://extensions with Developer mode enabled. Then use this workflow during testing:
- Open the page where the UI issue can be reproduced.
- Click the extension and choose Start capture.
- Perform the exact tester action, such as submitting an invalid form or expanding a menu.
- Click Stop capture.
- Add notes describing expected and actual behavior.
- Copy the evidence into the bug report and attach screenshots or video separately.
The copied evidence should make the report easier to triage. For example, a developer can see that the submit button gained disabled, the validation container was added, or the error banner was removed unexpectedly after an API response.
Screenshot Checklist
Capture these screens while following the workflow:
- The extension folder with
manifest.json,popup.html,popup.js, andobserver.js. - The unpacked extension loaded in
chrome://extensions. - The popup before starting capture.
- The application page before the tester action.
- The application page immediately after the DOM change.
- The popup showing copied evidence JSON.
- The final bug report with notes, screenshots, and DOM evidence attached.
Common Mistakes to Avoid
- Capturing too much data: Store summaries, not full page HTML. Full DOM dumps may include sensitive data and are hard to review.
- Claiming complete visual evidence: DOM changes do not prove every pixel-level visual change. Pair this output with screenshots or recordings.
- Ignoring privacy: Mask personal data before adding evidence to a shared ticket.
- Using broad permissions too early: Start with tester-triggered
activeTabaccess before adding host permissions. - Skipping validation: Reproduce the issue again after reviewing the evidence to confirm the captured change is related to the bug.
Best Practices for QA Teams
Use a simple bug report template so DOM evidence has a consistent place. A useful format is:
Observed DOM change evidence:
- Page:
- Tester action:
- Expected result:
- Actual result:
- Key DOM changes:
- Screenshot or video link:
- Environment:
For automation teams, this evidence can also seed better Playwright or Selenium assertions. If the extension shows that an error banner is added with a stable role or label, turn that into a deterministic assertion in the automated test. If the evidence is noisy or unstable, treat it as a signal to improve testability attributes in the application.
FAQ
Can this extension replace DevTools or Playwright traces?
No. It is a lightweight evidence helper. DevTools, Playwright traces, network logs, screenshots, and videos still provide stronger diagnostic context for many bugs.
Will MutationObserver capture every UI change?
No. It captures observable DOM mutations based on the observer configuration. Canvas drawing, CSS-only animations, browser accessibility tree behavior, and private framework state may not appear.
Is it safe to attach DOM evidence to every bug report?
Only after reviewing it. DOM text can contain customer names, emails, tokens, or internal data. Mask sensitive values before sharing.
Why use activeTab instead of host permissions?
activeTab fits a tester-initiated capture flow because access is granted after a user gesture on the active page. Broad host permissions may be valid for internal tools, but they require stricter review.
Conclusion
A Chrome extension can make UI bug reports more precise by recording the DOM changes that happen during a tester action. Keep the first version small: start capture, reproduce the issue, stop capture, add notes, and paste a compact evidence summary into the bug report. That gives QA engineers better handoff data without pretending the extension replaces real test execution, human review, or deeper debugging tools.

