1---
2name: qa-pass
3description: QA a live page across seven breakpoints. Checks content, links,
4 layout, accessibility and technical basics, then writes a prioritised report
5 with a paste-ready line per issue. Use before launch, after a redesign, or on
6 any page someone says looks off.
7argument-hint: <url> [quick|deep|focus:<category>]
8---
9
10# QA pass
11
12One URL in, one prioritised report out. Same checks every time, so two runs a
13month apart are comparable.
14
15**What you need:** the Playwright MCP server, and nothing else. No install into
16the site you are checking, no script to place, no repo access. It works against
17any URL you can load, including a site you did not build and cannot deploy to.
18
19## Step 1 - load the page in a real browser
20
21Drive Playwright, never a plain fetch. Half of what breaks only exists after
22JavaScript runs, and a fetch will not see it.
23
24Accept the cookie banner before anything else, with a text selector rather than
25a snapshot reference: snapshot refs go stale between calls and the click fails
26silently.
27
28## Step 2 - sweep seven breakpoints
29
30Smallest to largest, every run, whatever device the request named:
31
32| Width | Represents | Screenshot |
33| --- | --- | --- |
34| 330px | smallest common phone | yes |
35| 375px | mobile baseline | yes |
36| 768px | tablet portrait | yes |
37| 1024px | tablet landscape | overflow check only |
38| 1280px | desktop baseline | yes, full page |
39| 1440px | large desktop | overflow check only |
40| 1920px | full HD desktop | yes, full page |
41
42At each width run this through the MCP server's evaluate call. Run it as
43written rather than improvising the check, so a run today and a run in a month
44are answering the same question:
45
46```js
47() => ({
48 // A false here with elements listed below does NOT mean clipping: a
49 // container with overflow-x hidden suppresses the bar. Check the shot.
50 hasHorizontalScroll:
51 document.documentElement.scrollWidth > document.documentElement.clientWidth,
52 past: [...document.querySelectorAll("*")]
53 .filter((el) => el.getBoundingClientRect().right > window.innerWidth + 5)
54 .slice(0, 10)
55 .map((el) => ({
56 tag: el.tagName,
57 class: el.className?.toString().slice(0, 60),
58 right: Math.round(el.getBoundingClientRect().right),
59 })),
60})
61```
62
63These are representative widths, not every pixel. If someone reports a break at
64850px, resize to 850px and look, rather than trusting the sweep.
65
66Reset to 1280px before the content checks. Capture the accessibility tree and
67the console messages once, at that width.
68
69## Step 3 - the two rules that keep a report honest
70
71**Verify visually before reporting.** Code tells you what the DOM says, the
72screenshot tells you what a visitor sees. If you cannot see the problem in the
73screenshot, it does not go in the report.
74
75**Know the false positives.** Three that catch every new run:
76
77- No horizontal scrollbar plus overflowing elements does not mean clipping. A
78 container with overflow-x hidden suppresses the bar while the element still
79 extends past the viewport. Check the screenshot.
80- innerText returns empty for anything hidden or mid-animation, including links
81 inside a collapsed accordion. Read textContent before calling a link empty.
82- Keep a written ignore list of the patterns that are fine on this site, and
83 cite it in the report. A report that cries wolf twice gets skimmed the third
84 time.
85
86Expand every accordion individually, wait for the animation to settle, and
87screenshot the open state before reading what is inside.
88
89## Step 4 - check these, at 1280px
90
91**Content.** Spelling and grammar in visible copy, placeholder text left behind
92(lorem, TBD, {{key}}, %s), stale dates and old product names, inconsistent
93terminology, contradicting statements.
94
95**Links and CTAs.** Broken links, links pointing at staging or localhost, empty
96anchors with no text and no image, CTA text that says only "read more", a
97primary CTA that is missing or below the fold.
98
99**Layout.** Broken or stretched images, text overflow and truncation, misaligned
100or overlapping elements, inconsistent button styles, heading sizes that do not
101match the hierarchy.
102
103**Accessibility.** Images with no alt, links with no descriptive text, form
104inputs with no label, visibly low contrast, focus that disappears on tab.
105
106**Technical.** One evaluate covers everything a screenshot cannot show you.
107Read the console messages separately, and check for a consent banner where the
108law needs one:
109
110```js
111() => {
112 const meta = (sel, name) => document.querySelector(sel)?.getAttribute(name) ?? null;
113 return {
114 title: document.title,
115 description: meta('meta[name="description"]', "content"),
116 canonical: meta('link[rel="canonical"]', "href"),
117 robots: meta('meta[name="robots"]', "content"),
118 social: ["og:title", "og:description", "og:image", "twitter:card"].map((p) => ({
119 tag: p,
120 content: meta(`meta[property="${p}"], meta[name="${p}"]`, "content"),
121 })),
122 headings: [...document.querySelectorAll("h1,h2,h3,h4,h5,h6")].map((h) => ({
123 level: Number(h.tagName[1]),
124 // textContent, not innerText: innerText is empty for anything hidden or
125 // mid-animation, which turns every collapsed accordion into a false alarm.
126 text: h.textContent.trim().slice(0, 90),
127 })),
128 imagesWithoutAlt: [...document.images]
129 .filter((i) => !i.hasAttribute("alt"))
130 .map((i) => i.currentSrc),
131 vagueLinks: [...document.querySelectorAll("a")]
132 .map((a) => a.textContent.trim())
133 .filter((t) => /^(click here|read more|learn more|here|more)$/i.test(t)),
134 emptyLinks: [...document.querySelectorAll("a[href]")]
135 .filter((a) => !a.textContent.trim() && !a.querySelector("img"))
136 .map((a) => a.href),
137 offsiteLinks: [...document.querySelectorAll("a[href]")]
138 .map((a) => a.href)
139 .filter((h) => /staging\.|dev\.|localhost/.test(h)),
140 unnamedControls: [...document.querySelectorAll("button,input,select,textarea")]
141 .filter(
142 (el) =>
143 !el.labels?.length && !el.getAttribute("aria-label") && !el.textContent.trim(),
144 )
145 .map((el) => el.outerHTML.slice(0, 120)),
146 jsonLd: [...document.querySelectorAll('script[type="application/ld+json"]')].map(
147 (s) => s.textContent,
148 ),
149 };
150}
151```
152
153Title present and roughly 50-60 characters, meta description present, all four
154social tags present, canonical correct, no console errors.
155
156**Shared regions.** Check the footer and the global nav once for the whole site,
157not once per page. They come from one template, so the same three issues on
158forty pages is one issue.
159
160## Step 5 - report only what is broken
161
162Nothing optional, no best-practice suggestions, no "consider adding". If it is
163fine it does not appear. Give every issue an ID (EN1, EN2, and EN1M for a
164mobile-only one) so a reviewer can reply "EN3 is by design" in one line.
165
166| ID | Category | Priority | Location | Issue | Fix | Paste |
167| --- | --- | --- | --- | --- | --- | --- |
168
169The last column is the whole issue in under 200 characters, starting with the
170ID and ending with a sentence that starts "Fix:", so it drops straight into a
171message to whoever owns the page.
172
173Priorities: red for broken functionality, a typo in headline copy, a dead nav
174link or a severe accessibility failure. Yellow for anything a visitor will
175notice. Green and grey below that.
176
177Close with what is clean. The list of what passed is what makes the failures
178believable.
179
180## More than one language
181
182Every locale gets the same full checks, not a lighter scan. Practical order:
183establish the base language first, then the languages whose words run longest,
184where text expansion breaks layouts at 330 and 375px. Use one small evaluate
185per locale for title, H1 and a body excerpt, and pull a full snapshot only
186where that flags something.
187
188A fault in a template is one systemic issue affecting every locale, not one
189issue per locale. Say it once.
190
191Always state the limit: translation accuracy needs a native speaker. This pass
192covers structure, layout and completeness only.