QA testing
Run QA testing on a page, feature, or full site at one of three depth tiers (smoke, standard, full).
How to use it
Claude Code
- Run the line below. It pulls the whole folder into
~/.claude/skills/qa-testing, including the files SKILL.md points to. - Describe your job in plain words. Claude Code follows the skill from there.
npx degit rampstackco/claude-skills/skills/qa-testing#main ~/.claude/skills/qa-testingFor one project only, change the path to .claude/skills/qa-testing. This skill also uses robots.txt — copying SKILL.md alone won't be enough. See the folder on GitHub.
Claude (web or desktop app)
- On this page open ⋯ → Download .md.
- Save it as SKILL.md in a folder, zip the folder, then Customize → Skills → + → Create skill → Upload a skill.
- Pick the file and Save. Claude shows the name and description and runs a security scan.
- Check the skill is switched on.
- Start a new chat and describe your job in plain words. The AI follows the skill from there.
ChatGPT or another app
- ChatGPT: make a Project and paste it into Instructions.
- Neither? Paste it at the top of a new chat — it works for that chat.
Not working?
- Check which app you pasted it into — the steps above name the right one.
- Some skills need the paid tier of Claude or ChatGPT.
Paste into Claude, ChatGPT or Cursor.
Source of QA testing
Show the full text303 lines
| name | description | category | catalog_summary | display_order |
|---|---|---|---|---|
| qa-testing | Run QA testing on a page, feature, or full site at one of three depth tiers (smoke, standard, full). Use this skill whenever the user asks to QA a page or site, run a smoke test after a deploy, verify a page before launch, or run a regression sweep. Triggers on QA, QA sweep, smoke test, regression test, post-deploy check, pre-launch check, verify the deploy, test this page, does the page render, broken link, 404, image not loading. Also triggers proactively after a deploy or a new page launch where verification matters. Covers accessibility, performance, and SEO at the surface-signal level only: deep audits belong to `accessibility-audit`, `performance-optimization`, and `seo-technical`, and code-level debugging to `code-review-web`. | qa | Pre-launch QA, regression testing, cross-browser checks | 1 |
QA Testing
Verify that a page, feature, or site is working before declaring it shipped. Stack-agnostic. Console-snippet driven for speed.
This skill is faster than accessibility-audit (which goes deeper on WCAG) and performance-optimization (which goes deeper on Core Web Vitals). Use this skill for general QA. Use the specialists for deep audits.
When to use
- After every deploy (smoke test)
- After launching a new page or feature (standard audit)
- Before a major release (full release matrix)
- Investigating a "something looks off" report
- Pre-launch verification of a site or section
When NOT to use
- Deep accessibility compliance work (use
accessibility-audit) - Deep performance investigation (use
performance-optimization) - Code review or debugging (use
code-review-web) - Initial site setup or technical SEO baseline (use
seo-technical)
Required inputs
- The page URL or site under test
- The tier of QA needed (smoke, standard, or full)
- Browser dev tools access
- Any specific concerns to check beyond the standard tier
The framework: 3 tiers
QA scales with the stakes. Pick the tier that matches the context.
| Tier | When to run | Time | Coverage |
|---|---|---|---|
| Smoke | After every deploy | 2 minutes | Critical signals only |
| Standard | New page or feature | 10 minutes | On-page basics, accessibility, structure |
| Full | Major release, pre-launch | 30+ minutes | Comprehensive across all dimensions |
Tier 1: Smoke test
The 2-minute "did the deploy break anything obvious?" check. Run after every deploy.
Console snippet (paste in browser dev tools):
const smoke = {
title: document.title,
titleLen: document.title.length,
canonical: document.querySelector('link[rel="canonical"]')?.href,
h1Count: document.querySelectorAll('h1').length,
missingAlts: [...document.querySelectorAll('img')].filter(i => !i.hasAttribute('alt')).length,
schema: [...document.querySelectorAll('script[type="application/ld+json"]')]
.map(s => { try { return JSON.parse(s.innerText)['@type'] } catch(e) { return 'invalid' } }),
brokenImages: [...document.querySelectorAll('img')].filter(i => !i.complete || i.naturalWidth === 0).length,
};
console.log(JSON.stringify(smoke, null, 2));
Pass criteria:
- Title exists and is 30 to 60 characters
- Canonical points at the production domain (never staging or preview URLs)
- Exactly one H1
- Zero images missing the
altattribute. An emptyalt=""on a decorative image is correct markup and passes; only an absent attribute fails. - Zero broken images
- Every schema block parses (no
invalidentries in the snippet output). Whether the types are the right ones for the page is a Full-tier check, against the Rich Results Test.
If any of these fail, do not proceed with deeper testing until the smoke issue is fixed.
Tier 2: Standard page audit
The 10-minute new-page-or-feature audit. Covers the on-page basics plus accessibility and structure.
Console snippet:
const audit = {
title: document.title,
titleLen: document.title.length,
canonical: document.querySelector('link[rel="canonical"]')?.href,
metaDesc: document.querySelector('meta[name="description"]')?.content,
metaDescLen: document.querySelector('meta[name="description"]')?.content?.length,
ogImage: document.querySelector('meta[property="og:image"]')?.content,
ogTitle: document.querySelector('meta[property="og:title"]')?.content,
twitterCard: document.querySelector('meta[name="twitter:card"]')?.content,
h1Count: document.querySelectorAll('h1').length,
h1Text: document.querySelector('h1')?.innerText,
h2Count: document.querySelectorAll('h2').length,
h2s: [...document.querySelectorAll('h2')].map(h => h.innerText.trim().slice(0, 60)),
totalImages: document.querySelectorAll('img').length,
missingAlts: [...document.querySelectorAll('img')].filter(i => !i.hasAttribute('alt')).length,
brokenImages: [...document.querySelectorAll('img')].filter(i => !i.complete || i.naturalWidth === 0).length,
externalLinksWithoutNoopener: [...document.querySelectorAll('a[target="_blank"]')]
.filter(a => !a.rel?.includes('noopener')).length,
schema: [...document.querySelectorAll('script[type="application/ld+json"]')]
.map(s => {
try {
const d = JSON.parse(s.innerText);
return d['@graph'] ? d['@graph'].map(x => x['@type']) : d['@type'];
} catch(e) { return 'invalid' }
}),
hasSkipLink: [...document.querySelectorAll('a[href^="#"]')].slice(0, 3)
.some(a => /skip/i.test(a.textContent) && !!document.getElementById(a.getAttribute('href').slice(1))),
pageLanguage: document.documentElement.lang || 'NOT SET',
hasFavicon: !!document.querySelector('link[rel*="icon"]'),
};
console.log(JSON.stringify(audit, null, 2));
Pass criteria (in addition to smoke):
- Meta description: 120 to 160 characters
- og:image, og:title, twitter:card present
- H2s present and descriptive
- All external links with
target="_blank"haverel="noopener" - Page language declared (
langattribute on<html>) - Favicon present
Tier 3: Full release matrix
The 30-minute pre-launch check. Cover all dimensions.
| Dimension | Pass criteria |
|---|---|
| Smoke and standard | All pass |
| Accessibility (basic) | Run browser audit tool (e.g., Lighthouse), score above 90 |
| Performance (basic) | Every threshold in the report template's Performance checklist, INP included |
| Mobile responsiveness | Every viewport in the report template's responsiveness checklist |
| Cross-browser | Tested in Chrome, Safari, Firefox (and Edge if relevant audience) |
| Forms | All forms submit successfully and validate correctly |
| Internal links | No broken internal links (sample 20 random) |
| External links | All return 200 (sample 10) |
| Sitemap | Returns 200, lists canonical URLs only |
| robots.txt | Allows production crawlers, blocks staging if applicable |
| Security headers | HSTS, X-Frame-Options, X-Content-Type-Options present |
| HTTPS | All resources load over HTTPS, no mixed content |
| 404 handling | 404 pages return HTTP 404 (not soft 200) |
| Schema validation | All schema validates in Rich Results Test |
| Analytics | Events fire as expected on key user actions |
| Cache behavior | Cache headers appropriate for page type |
For headers, run:
fetch(window.location.origin, { method: 'HEAD' })
.then(r => {
const headers = {};
for (const [k, v] of r.headers.entries()) headers[k] = v;
console.log(JSON.stringify(headers, null, 2));
});
Look for: strict-transport-security, x-frame-options, x-content-type-options.
Specific QA snippets
Image audit
const imgs = [...document.querySelectorAll('img')].map(i => ({
src: i.src.split('/').pop().split('?')[0].slice(0, 60),
alt: i.hasAttribute('alt') ? (i.alt === '' ? 'DECORATIVE (empty alt)' : i.alt) : 'MISSING',
width: i.naturalWidth,
height: i.naturalHeight,
loaded: i.complete && i.naturalWidth > 0,
}));
console.table(imgs);
console.log({
total: imgs.length,
broken: imgs.filter(i => !i.loaded).length,
noAlt: imgs.filter(i => i.alt === 'MISSING').length,
decorative: imgs.filter(i => i.alt.startsWith('DECORATIVE')).length,
});
Heading hierarchy check
const headings = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].map(h => ({
level: parseInt(h.tagName[1]),
text: h.innerText.trim().slice(0, 80),
}));
console.table(headings);
// Check for skipped levels
const levels = headings.map(h => h.level);
let skipped = false;
for (let i = 1; i < levels.length; i++) {
if (levels[i] > levels[i-1] + 1) {
console.warn(`Skipped from H${levels[i-1]} to H${levels[i]}: "${headings[i].text}"`);
skipped = true;
}
}
if (!skipped) console.log('No skipped heading levels');
Contrast spot-check
function contrast(bg, fg) {
function lum(hex) {
return [hex.slice(1,3), hex.slice(3,5), hex.slice(5,7)]
.map(h => parseInt(h, 16) / 255)
.map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4))
.reduce((a, v, i) => a + v * [0.2126, 0.7152, 0.0722][i], 0);
}
const [l1, l2] = [lum(bg), lum(fg)];
const r = ((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)).toFixed(2);
return r + ':1 ' + (parseFloat(r) >= 4.5 ? 'PASS body' : parseFloat(r) >= 3 ? 'PASS large only' : 'FAIL');
}
// Examples
contrast('#FFFFFF', '#4B5563'); // body color check
contrast('#FFFFFF', '#9CA3AF'); // verify gray choices
Form audit (per form)
[...document.querySelectorAll('form')].forEach((form, i) => {
const fields = [...form.querySelectorAll('input, select, textarea')].map(field => ({
type: field.type || field.tagName.toLowerCase(),
name: field.name,
hasLabel: !!form.querySelector(`label[for="${field.id}"]`) || !!field.closest('label'),
required: field.required,
}));
console.log(`Form ${i + 1}:`);
console.table(fields);
});
External link audit
const externalLinks = [...document.querySelectorAll('a[href^="http"]')]
.filter(a => !a.href.includes(window.location.host));
const issues = externalLinks.filter(a =>
a.target === '_blank' && (!a.rel?.includes('noopener') || !a.rel?.includes('noreferrer'))
);
if (issues.length) {
console.warn(`${issues.length} external links missing noopener/noreferrer:`);
issues.forEach(a => console.warn(a.href));
} else {
console.log(`All ${externalLinks.length} external links properly attributed`);
}
Workflow
- Pick the tier. Smoke for routine deploys. Standard for new work. Full for releases.
- Run the snippet. Paste the appropriate console snippet, review output.
- Note failures. Each failure either gets fixed before ship or filed as a known issue.
- For Standard tier, add: visual review at 375px, 768px, and 1440px. Standard deliberately skips 1024px; the Full tier picks it up with the rest of the template's responsiveness checklist. Test the primary user flow.
- For Full tier, add: cross-browser testing, Lighthouse audit, schema validation, security headers, 404 handling.
- Document. Use the template in
references/qa-report-template.mdfor full audits.
Failure patterns
- Skipping smoke tests on "small" deploys. Half of broken-production incidents start with a deploy that "looked safe."
- Running snippets but not reading the output. The console snippet is a tool. The judgment is reading what it returns.
- Visual-only QA. Eyeballing a page misses missing alt text, broken schema, missing canonical. Always run the snippet.
- Single-browser testing. Mobile Safari and Chrome differ enough to surprise you. Test at least Chrome and Safari.
- No mobile QA. The 375px viewport is where most users live. Test there or get burned later.
- Pass-fail with no remediation. A failed QA must produce a fix or a known-issue ticket. Failed QA that ships unfixed is process theater.
Output format
For smoke tests: console output is the report.
For standard and full audits: a markdown report at qa-report-[date].md. Use the template in references/qa-report-template.md.
If required data is unavailable
This skill's output depends on data, measurements, or tool results it cannot generate on its own. When a required input, tool, or data source is unavailable or unverifiable, the sanctioned output is the deliverable with the gap stated: what was needed, what was actually obtained or verified, and which parts of the output are affected. Fabricating, estimating, or interpolating a required number to complete the deliverable is never sanctioned. A stated gap is a complete answer.
Reference files
references/qa-report-template.md- Markdown report template for standard and full audits.
| 1 | |
| 2 | name qa-testing |
| 3 | description "Run QA testing on a page, feature, or full site at one of three depth tiers (smoke, standard, full). Use this skill whenever the user asks to QA a page or site, run a smoke test after a deploy, verify a page before launch, or run a regression sweep. Triggers on QA, QA sweep, smoke test, regression test, post-deploy check, pre-launch check, verify the deploy, test this page, does the page render, broken link, 404, image not loading. Also triggers proactively after a deploy or a new page launch where verification matters. Covers accessibility, performance, and SEO at the surface-signal level only: deep audits belong to `accessibility-audit`, `performance-optimization`, and `seo-technical`, and code-level debugging to `code-review-web`." |
| 4 | category qa |
| 5 | catalog_summary "Pre-launch QA, regression testing, cross-browser checks" |
| 6 | display_order 1 |
| 7 | |
| 8 | |
| 9 | # QA Testing |
| 10 | |
| 11 | Verify that a page, feature, or site is working before declaring it shipped. Stack-agnostic. Console-snippet driven for speed. |
| 12 | |
| 13 | This skill is faster than `accessibility-audit` (which goes deeper on WCAG) and `performance-optimization` (which goes deeper on Core Web Vitals). Use this skill for general QA. Use the specialists for deep audits. |
| 14 | |
| 15 | |
| 16 | |
| 17 | ## When to use |
| 18 | |
| 19 | After every deploy (smoke test) |
| 20 | After launching a new page or feature (standard audit) |
| 21 | Before a major release (full release matrix) |
| 22 | Investigating a "something looks off" report |
| 23 | Pre-launch verification of a site or section |
| 24 | |
| 25 | ## When NOT to use |
| 26 | |
| 27 | Deep accessibility compliance work (use `accessibility-audit`) |
| 28 | Deep performance investigation (use `performance-optimization`) |
| 29 | Code review or debugging (use `code-review-web`) |
| 30 | Initial site setup or technical SEO baseline (use `seo-technical`) |
| 31 | |
| 32 | |
| 33 | |
| 34 | ## Required inputs |
| 35 | |
| 36 | The page URL or site under test |
| 37 | The tier of QA needed (smoke, standard, or full) |
| 38 | Browser dev tools access |
| 39 | Any specific concerns to check beyond the standard tier |
| 40 | |
| 41 | |
| 42 | |
| 43 | ## The framework: 3 tiers |
| 44 | |
| 45 | QA scales with the stakes. Pick the tier that matches the context. |
| 46 | |
| 47 | | Tier | When to run | Time | Coverage | |
| 48 | |---|---|---|---| |
| 49 | | Smoke | After every deploy | 2 minutes | Critical signals only | |
| 50 | | Standard | New page or feature | 10 minutes | On-page basics, accessibility, structure | |
| 51 | | Full | Major release, pre-launch | 30+ minutes | Comprehensive across all dimensions | |
| 52 | |
| 53 | ### Tier 1: Smoke test |
| 54 | |
| 55 | The 2-minute "did the deploy break anything obvious?" check. Run after every deploy. |
| 56 | |
| 57 | Console snippet (paste in browser dev tools): |
| 58 | |
| 59 | |
| 60 | const smoke = { |
| 61 | title: document.title, |
| 62 | titleLen: document.title.length, |
| 63 | canonical: document.querySelector('link[rel="canonical"]')?.href, |
| 64 | h1Count: document.querySelectorAll('h1').length, |
| 65 | missingAlts: [...document.querySelectorAll('img')].filter(i => !i.hasAttribute('alt')).length, |
| 66 | schema: [...document.querySelectorAll('script[type="application/ld+json"]')] |
| 67 | .map(s => { try { return JSON.parse(s.innerText)['@type'] } catch(e) { return 'invalid' } }), |
| 68 | brokenImages: [...document.querySelectorAll('img')].filter(i => !i.complete || i.naturalWidth === 0).length, |
| 69 | }; |
| 70 | console.log(JSON.stringify(smoke, null, 2)); |
| 71 | |
| 72 | |
| 73 | **Pass criteria:** |
| 74 | Title exists and is 30 to 60 characters |
| 75 | Canonical points at the production domain (never staging or preview URLs) |
| 76 | Exactly one H1 |
| 77 | Zero images missing the `alt` attribute. An empty `alt=""` on a decorative image is correct markup and passes; only an absent attribute fails. |
| 78 | Zero broken images |
| 79 | Every schema block parses (no `invalid` entries in the snippet output). Whether the types are the right ones for the page is a Full-tier check, against the Rich Results Test. |
| 80 | |
| 81 | If any of these fail, do not proceed with deeper testing until the smoke issue is fixed. |
| 82 | |
| 83 | ### Tier 2: Standard page audit |
| 84 | |
| 85 | The 10-minute new-page-or-feature audit. Covers the on-page basics plus accessibility and structure. |
| 86 | |
| 87 | Console snippet: |
| 88 | |
| 89 | |
| 90 | const audit = { |
| 91 | title: document.title, |
| 92 | titleLen: document.title.length, |
| 93 | canonical: document.querySelector('link[rel="canonical"]')?.href, |
| 94 | metaDesc: document.querySelector('meta[name="description"]')?.content, |
| 95 | metaDescLen: document.querySelector('meta[name="description"]')?.content?.length, |
| 96 | ogImage: document.querySelector('meta[property="og:image"]')?.content, |
| 97 | ogTitle: document.querySelector('meta[property="og:title"]')?.content, |
| 98 | twitterCard: document.querySelector('meta[name="twitter:card"]')?.content, |
| 99 | h1Count: document.querySelectorAll('h1').length, |
| 100 | h1Text: document.querySelector('h1')?.innerText, |
| 101 | h2Count: document.querySelectorAll('h2').length, |
| 102 | h2s: [...document.querySelectorAll('h2')].map(h => h.innerText.trim().slice(0, 60)), |
| 103 | totalImages: document.querySelectorAll('img').length, |
| 104 | missingAlts: [...document.querySelectorAll('img')].filter(i => !i.hasAttribute('alt')).length, |
| 105 | brokenImages: [...document.querySelectorAll('img')].filter(i => !i.complete || i.naturalWidth === 0).length, |
| 106 | externalLinksWithoutNoopener: [...document.querySelectorAll('a[target="_blank"]')] |
| 107 | .filter(a => !a.rel?.includes('noopener')).length, |
| 108 | schema: [...document.querySelectorAll('script[type="application/ld+json"]')] |
| 109 | .map(s => { |
| 110 | try { |
| 111 | const d = JSON.parse(s.innerText); |
| 112 | return d['@graph'] ? d['@graph'].map(x => x['@type']) : d['@type']; |
| 113 | } catch(e) { return 'invalid' } |
| 114 | }), |
| 115 | hasSkipLink: [...document.querySelectorAll('a[href^="#"]')].slice(0, 3) |
| 116 | .some(a => /skip/i.test(a.textContent) && !!document.getElementById(a.getAttribute('href').slice(1))), |
| 117 | pageLanguage: document.documentElement.lang || 'NOT SET', |
| 118 | hasFavicon: !!document.querySelector('link[rel*="icon"]'), |
| 119 | }; |
| 120 | console.log(JSON.stringify(audit, null, 2)); |
| 121 | |
| 122 | |
| 123 | **Pass criteria** (in addition to smoke): |
| 124 | Meta description: 120 to 160 characters |
| 125 | og:image, og:title, twitter:card present |
| 126 | H2s present and descriptive |
| 127 | All external links with `target="_blank"` have `rel="noopener"` |
| 128 | Page language declared (`lang` attribute on `<html>`) |
| 129 | Favicon present |
| 130 | |
| 131 | ### Tier 3: Full release matrix |
| 132 | |
| 133 | The 30-minute pre-launch check. Cover all dimensions. |
| 134 | |
| 135 | | Dimension | Pass criteria | |
| 136 | |---|---| |
| 137 | | Smoke and standard | All pass | |
| 138 | | Accessibility (basic) | Run browser audit tool (e.g., Lighthouse), score above 90 | |
| 139 | | Performance (basic) | Every threshold in the report template's Performance checklist, INP included | |
| 140 | | Mobile responsiveness | Every viewport in the report template's responsiveness checklist | |
| 141 | | Cross-browser | Tested in Chrome, Safari, Firefox (and Edge if relevant audience) | |
| 142 | | Forms | All forms submit successfully and validate correctly | |
| 143 | | Internal links | No broken internal links (sample 20 random) | |
| 144 | | External links | All return 200 (sample 10) | |
| 145 | | Sitemap | Returns 200, lists canonical URLs only | |
| 146 | | robots.txt | Allows production crawlers, blocks staging if applicable | |
| 147 | | Security headers | HSTS, X-Frame-Options, X-Content-Type-Options present | |
| 148 | | HTTPS | All resources load over HTTPS, no mixed content | |
| 149 | | 404 handling | 404 pages return HTTP 404 (not soft 200) | |
| 150 | | Schema validation | All schema validates in Rich Results Test | |
| 151 | | Analytics | Events fire as expected on key user actions | |
| 152 | | Cache behavior | Cache headers appropriate for page type | |
| 153 | |
| 154 | For headers, run: |
| 155 | |
| 156 | |
| 157 | fetch(window.location.origin, { method: 'HEAD' }) |
| 158 | .then(r => { |
| 159 | const headers = {}; |
| 160 | for (const [k, v] of r.headers.entries()) headers[k] = v; |
| 161 | console.log(JSON.stringify(headers, null, 2)); |
| 162 | }); |
| 163 | |
| 164 | |
| 165 | Look for: `strict-transport-security`, `x-frame-options`, `x-content-type-options`. |
| 166 | |
| 167 | |
| 168 | |
| 169 | ## Specific QA snippets |
| 170 | |
| 171 | ### Image audit |
| 172 | |
| 173 | |
| 174 | const imgs = [...document.querySelectorAll('img')].map(i => ({ |
| 175 | src: i.src.split('/').pop().split('?')[0].slice(0, 60), |
| 176 | alt: i.hasAttribute('alt') ? (i.alt === '' ? 'DECORATIVE (empty alt)' : i.alt) : 'MISSING', |
| 177 | width: i.naturalWidth, |
| 178 | height: i.naturalHeight, |
| 179 | loaded: i.complete && i.naturalWidth > 0, |
| 180 | })); |
| 181 | console.table(imgs); |
| 182 | console.log({ |
| 183 | total: imgs.length, |
| 184 | broken: imgs.filter(i => !i.loaded).length, |
| 185 | noAlt: imgs.filter(i => i.alt === 'MISSING').length, |
| 186 | decorative: imgs.filter(i => i.alt.startsWith('DECORATIVE')).length, |
| 187 | }); |
| 188 | |
| 189 | |
| 190 | ### Heading hierarchy check |
| 191 | |
| 192 | |
| 193 | const headings = [...document.querySelectorAll('h1, h2, h3, h4, h5, h6')].map(h => ({ |
| 194 | level: parseInt(h.tagName[1]), |
| 195 | text: h.innerText.trim().slice(0, 80), |
| 196 | })); |
| 197 | console.table(headings); |
| 198 | |
| 199 | // Check for skipped levels |
| 200 | const levels = headings.map(h => h.level); |
| 201 | let skipped = false; |
| 202 | for (let i = 1; i < levels.length; i++) { |
| 203 | if (levels[i] > levels[i-1] + 1) { |
| 204 | console.warn(`Skipped from H${levels[i-1]} to H${levels[i]}: "${headings[i].text}"`); |
| 205 | skipped = true; |
| 206 | } |
| 207 | } |
| 208 | if (!skipped) console.log('No skipped heading levels'); |
| 209 | |
| 210 | |
| 211 | ### Contrast spot-check |
| 212 | |
| 213 | |
| 214 | function contrast(bg, fg) { |
| 215 | function lum(hex) { |
| 216 | return [hex.slice(1,3), hex.slice(3,5), hex.slice(5,7)] |
| 217 | .map(h => parseInt(h, 16) / 255) |
| 218 | .map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)) |
| 219 | .reduce((a, v, i) => a + v * [0.2126, 0.7152, 0.0722][i], 0); |
| 220 | } |
| 221 | const [l1, l2] = [lum(bg), lum(fg)]; |
| 222 | const r = ((Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05)).toFixed(2); |
| 223 | return r + ':1 ' + (parseFloat(r) >= 4.5 ? 'PASS body' : parseFloat(r) >= 3 ? 'PASS large only' : 'FAIL'); |
| 224 | } |
| 225 | |
| 226 | // Examples |
| 227 | contrast('#FFFFFF', '#4B5563'); // body color check |
| 228 | contrast('#FFFFFF', '#9CA3AF'); // verify gray choices |
| 229 | |
| 230 | |
| 231 | ### Form audit (per form) |
| 232 | |
| 233 | |
| 234 | [...document.querySelectorAll('form')].forEach((form, i) => { |
| 235 | const fields = [...form.querySelectorAll('input, select, textarea')].map(field => ({ |
| 236 | type: field.type || field.tagName.toLowerCase(), |
| 237 | name: field.name, |
| 238 | hasLabel: !!form.querySelector(`label[for="${field.id}"]`) || !!field.closest('label'), |
| 239 | required: field.required, |
| 240 | })); |
| 241 | console.log(`Form ${i + 1}:`); |
| 242 | console.table(fields); |
| 243 | }); |
| 244 | |
| 245 | |
| 246 | ### External link audit |
| 247 | |
| 248 | |
| 249 | const externalLinks = [...document.querySelectorAll('a[href^="http"]')] |
| 250 | .filter(a => !a.href.includes(window.location.host)); |
| 251 | const issues = externalLinks.filter(a => |
| 252 | a.target === '_blank' && (!a.rel?.includes('noopener') || !a.rel?.includes('noreferrer')) |
| 253 | ); |
| 254 | if (issues.length) { |
| 255 | console.warn(`${issues.length} external links missing noopener/noreferrer:`); |
| 256 | issues.forEach(a => console.warn(a.href)); |
| 257 | } else { |
| 258 | console.log(`All ${externalLinks.length} external links properly attributed`); |
| 259 | } |
| 260 | |
| 261 | |
| 262 | |
| 263 | |
| 264 | ## Workflow |
| 265 | |
| 266 | **Pick the tier.** Smoke for routine deploys. Standard for new work. Full for releases. |
| 267 | **Run the snippet.** Paste the appropriate console snippet, review output. |
| 268 | **Note failures.** Each failure either gets fixed before ship or filed as a known issue. |
| 269 | **For Standard tier**, add: visual review at 375px, 768px, and 1440px. Standard deliberately skips 1024px; the Full tier picks it up with the rest of the template's responsiveness checklist. Test the primary user flow. |
| 270 | **For Full tier**, add: cross-browser testing, Lighthouse audit, schema validation, security headers, 404 handling. |
| 271 | **Document.** Use the template in [`references/qa-report-template.md`] for full audits. |
| 272 | |
| 273 | |
| 274 | |
| 275 | ## Failure patterns |
| 276 | |
| 277 | **Skipping smoke tests on "small" deploys.** Half of broken-production incidents start with a deploy that "looked safe." |
| 278 | **Running snippets but not reading the output.** The console snippet is a tool. The judgment is reading what it returns. |
| 279 | **Visual-only QA.** Eyeballing a page misses missing alt text, broken schema, missing canonical. Always run the snippet. |
| 280 | **Single-browser testing.** Mobile Safari and Chrome differ enough to surprise you. Test at least Chrome and Safari. |
| 281 | **No mobile QA.** The 375px viewport is where most users live. Test there or get burned later. |
| 282 | **Pass-fail with no remediation.** A failed QA must produce a fix or a known-issue ticket. Failed QA that ships unfixed is process theater. |
| 283 | |
| 284 | |
| 285 | |
| 286 | ## Output format |
| 287 | |
| 288 | For smoke tests: console output is the report. |
| 289 | |
| 290 | For standard and full audits: a markdown report at `qa-report-[date].md`. Use the template in [`references/qa-report-template.md`]. |
| 291 | |
| 292 | |
| 293 | |
| 294 | ## If required data is unavailable |
| 295 | |
| 296 | This skill's output depends on data, measurements, or tool results it cannot generate on its own. When a required input, tool, or data source is unavailable or unverifiable, the sanctioned output is the deliverable with the gap stated: what was needed, what was actually obtained or verified, and which parts of the output are affected. Fabricating, estimating, or interpolating a required number to complete the deliverable is never sanctioned. A stated gap is a complete answer. |
| 297 | |
| 298 | |
| 299 | |
| 300 | ## Reference files |
| 301 | |
| 302 | [`references/qa-report-template.md`] - Markdown report template for standard and full audits. |
| 303 |
Discussion
Browse more free Claude skills or everything in Marketing.



