The AI Web
QA pass logo

Skills

QA pass

Put a page through the same checks every time: how it holds up at seven screen widths, what the markup says, whether anyone on a keyboard can get through it, and whether the words are right.

updated 03 Sept·Claude Code skill driving Playwright through its MCP server

When do you reach for QA pass?

Reach for it before you publish, after a redesign, and on the page a client says looks off without being able to say why. What comes back is a list of faults with a fix attached to each one, not a folder of screenshots you still have to read yourself.

What does a QA pass check?

Six areas, in the same order every run: content, links and CTAs, layout, accessibility, technical basics, and anything that comes from a shared template. The list is fixed on purpose, so a run today and a run in a month are comparable and nothing gets checked only when somebody happens to remember it.

  • Content: spelling, leftover placeholder text, stale dates, terminology that changed halfway down the page.
  • Links and CTAs: dead links, links still pointing at staging, empty anchors, a primary CTA that is missing or below the fold.
  • Layout: text overflow, overlapping elements, and sideways scroll at any of the seven widths.
  • Accessibility: images with no alt, controls with no label, low contrast, focus that vanishes when you tab.
  • Technical: title, meta description, canonical, the four social tags, console errors.
  • Shared regions: the footer and global nav get checked once for the whole site, not once per page.

How does it use Playwright?

Playwright loads the page in a real browser, because half of what breaks only exists after JavaScript has run and a plain fetch never sees it. It accepts the cookie banner first, then sweeps seven widths from 330px to 1920px, screenshotting five of them and running an overflow check at all seven. Accordions get opened one at a time and screenshotted open, since content behind a collapsed panel reads as missing when it is only hidden.

How does it avoid crying wolf?

One rule above the others: verify visually before reporting. Code tells you what the DOM says, the screenshot tells you what a visitor sees, and anything you cannot see in the screenshot does not go in the report. Three known false positives are handled by name, including the big one, where a container hiding its overflow makes an element look clipped when nothing is wrong. The skill also keeps a written ignore list of patterns that are fine on your site, because a report that raises two false alarms gets skimmed the third time.

What comes back?

Only what is broken. No best-practice suggestions, nothing optional, nothing that passed. Every issue gets an ID, a priority, where it is, and a fix, plus a column holding the whole thing in under 200 characters so it can be pasted straight into a message to whoever owns the page. It closes with what is clean, which is what makes the failures believable.

What you copy

One block, ready to paste. Nothing else to install unless the block says so.

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.

Free to use in your own work, paid work included, no attribution required. Not for repackaging into a product you sell. Full terms.

Taggedclaude codeplaywrightpre-launchaccessibility

More in skills

Back to every skills entry.