From 0bfa52c06daf9f59fd535097dca5b532303a0a47 Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Thu, 9 Jul 2026 08:51:27 -0400 Subject: [PATCH 1/2] Detect keyword stuffing (SEO abuse) in alt-text-quality --- src/judges/prompt.ts | 28 +++- src/rules/alt-text-quality.ts | 8 ++ .../alt-quality/cases-keyword-stuffing.json | 128 ++++++++++++++++++ tests/unit/alt-text-quality.test.ts | 15 ++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/alt-quality/cases-keyword-stuffing.json diff --git a/src/judges/prompt.ts b/src/judges/prompt.ts index 4d009bd..5cc0293 100644 --- a/src/judges/prompt.ts +++ b/src/judges/prompt.ts @@ -29,22 +29,24 @@ A reliable signal: if the provided context calls the adjacent text a "caption", - If current alt is non-empty → verdict = "decorative" ## Step 3 — Functional (image inside a link or button) -The image is the only content of a link or button. The alt becomes the link's accessible name. Apply criteria F–H below. The alt is "ok" if and only if ALL hold: +The image is the only content of a link or button. The alt becomes the link's accessible name. Apply criteria F–I below. The alt is "ok" if and only if ALL hold: F. NAMES-THE-TARGET — The alt names what the user will reach (the entity, page, or action). Naming the entity (e.g., "Astronaut Ellen Ochoa" for a link to her bio) qualifies; the alt does NOT need to use the literal word "link" or "page". G. NOT-MEDIUM-ANNOUNCING — The alt does not announce the destination's medium or technology ("Wikipedia entry for …", "PDF of …", "Click here to read about …", "Link to …"). Screen readers already announce the link role. H. NOT-GENERIC — The alt is not empty and not a placeholder like "Read More", "Image", "Click here". + I. NOT-KEYWORD-STUFFED — The alt names the target, not a list of search keywords (see R6). If any criterion fails → verdict = "needs-fix". Otherwise → verdict = "ok". ## Step 4 — Informative (default) -The image conveys information that contributes meaning to the page. Apply criteria A–E below. The alt is "ok" if and only if ALL hold: +The image conveys information that contributes meaning to the page. Apply criteria A–F below. The alt is "ok" if and only if ALL hold: A. ACCURATE — The alt describes what the image actually shows in this context. B. CONTEXTUALLY-COMPLETE — The alt captures the content the page relies on the image to convey, given the surrounding text. If the surrounding text already covers some of that content, the alt may be brief. C. NOT-DUPLICATIVE — The alt does not repeat sentences from nearby text verbatim. D. NOT-INTRUSIVE — The alt does not introduce new claims absent from the page (no extra trivia, no marketing copy). E. APPROPRIATELY-FRAMED — Vocabulary, register, and detail level match the audience and genre indicated by the context. + F. NOT-KEYWORD-STUFFED — The alt is a genuine description, not a list of search keywords (see R6). If any criterion fails → verdict = "needs-fix". Otherwise → verdict = "ok". @@ -64,6 +66,10 @@ R4. **An empty alt ("") is the correct answer when (and only when) the image mat R5. **Do not overthink.** Walk the decision procedure once, in order. Do not invent context that wasn't provided. Do not bring in general knowledge about what "good" alt text usually looks like beyond the rules above. Pick the first matching step and stop. +R6. **Keyword stuffing is a failure (SEO abuse).** Some authors pack the alt attribute with search keywords instead of describing the image. This is criterion F (Step 4) / I (Step 3) and yields verdict = "needs-fix" with issue = "keyword-stuffing". + - Signals: the alt reads as a list rather than a phrase or sentence; comma- or pipe-separated keyword runs; the same term or near-duplicates repeated; multiple unrelated products, brands, or categories; commercial terms ("buy", "cheap", "best … 2026", "for sale"); or tokens unsupported by the image, the Azure tags/OCR (when provided), or the surrounding context. + - Do NOT confuse stuffing with legitimately long or detailed alt. Per R1/R1a, a long alt is correct when the context warrants it (labeled diagrams, art, educational figures) and the words genuinely describe THIS image. Length alone is never stuffing, and a comma-separated list is fine when it enumerates the image's real content (e.g., the labeled stages of a diagram). The test is whether the words describe the image or merely list keywords for ranking. + # Output Return a single JSON object with EXACTLY these fields, in this order: step, reasoning, verdict, issue, confidence. @@ -128,4 +134,22 @@ Return a single JSON object with EXACTLY these fields, in this order: step, reas An art-history textbook chapter on artistic technique. The surrounding paragraph analyzes the painting's use of light, color, form, perspective, proportion, and motion to depict the iconic crossing. Painting of George Washington crossing the Delaware River {"step":4,"reasoning":"Step 4 matches and all of A–E hold. Per R1a, the surrounding paragraph's analysis of light/color/form is not a requirement that the alt reproduce that analysis; the alt names what the image is, and the surrounding text supplies the artistic analysis. Per R2, 'Painting of' is a semantic prefix.","verdict":"ok","issue":"","confidence":0.9} + + + +Product page for a running shoe. The image shows a single blue running shoe. Surrounding body text describes the shoe's cushioning. The image is not inside a link. +running shoes, blue running shoes, best running shoes 2026, cheap running shoes, buy shoes online, marathon shoes, nike adidas shoes, mens womens shoes, shoe sale +{"step":4,"reasoning":"Step 4 matches but criterion F fails per R6: the alt is a comma-separated list of search keywords — 'shoes' repeated, unrelated brands, and commercial terms like 'buy' and 'sale' — rather than a description of the single blue running shoe shown. This is keyword stuffing.","verdict":"needs-fix","issue":"keyword-stuffing","confidence":0.95} + + + +Blog post about a hiking trail. The image shows a wooden trail sign in a forest. The image is not inside a link. +Wooden trail sign in a forest, hiking, camping, national parks, outdoor gear, best hiking boots, travel tips, adventure tourism +{"step":4,"reasoning":"Step 4 matches but criterion F fails per R6: the alt opens with an accurate description ('wooden trail sign in a forest') then appends unrelated SEO keywords (camping, outdoor gear, hiking boots, travel tips) the image does not depict. The trailing keyword pile-up is stuffing.","verdict":"needs-fix","issue":"keyword-stuffing","confidence":0.9} + + + +Biology textbook figure. The image is a labeled diagram showing the stages of mitosis in order. +Mitosis stages: Prophase, Prometaphase, Metaphase, Anaphase, and Telophase +{"step":4,"reasoning":"Step 4 matches and all of A–F hold. Although the alt is comma-separated, per R6 it is not stuffing: it enumerates the diagram's actual labeled content (the mitosis stages) rather than listing search keywords. Per R3, naming the labels is the correct alt content.","verdict":"ok","issue":"","confidence":0.9} ` diff --git a/src/rules/alt-text-quality.ts b/src/rules/alt-text-quality.ts index f09b774..8fd6731 100644 --- a/src/rules/alt-text-quality.ts +++ b/src/rules/alt-text-quality.ts @@ -73,6 +73,14 @@ function verdictToResult(image: ImageRecord, verdict: JudgeVerdict): RuleResult } case 'needs-fix': + if (verdict.issue === 'keyword-stuffing') { + return { + image, + problemShort: `Alt text appears keyword-stuffed for SEO rather than describing the image:\n"${image.alt ?? ''}"`, + solutionShort: 'Replace the keyword list with a concise description of what the image actually shows.', + solutionLong: verdict.reasoning, + } + } return { image, problemShort: `Alt text quality issue${verdict.issue ? ` (${verdict.issue})` : ''}:\n"${image.alt ?? ''}"`, diff --git a/tests/fixtures/alt-quality/cases-keyword-stuffing.json b/tests/fixtures/alt-quality/cases-keyword-stuffing.json new file mode 100644 index 0000000..0b5685f --- /dev/null +++ b/tests/fixtures/alt-quality/cases-keyword-stuffing.json @@ -0,0 +1,128 @@ +[ + { + "id": "kw-ellen-ochoa-stuffed-blatant", + "image": "https://webaim.org/techniques/alttext/media/ellen-ochoa.jpg", + "alt": "Ellen Ochoa, astronaut, NASA, space shuttle, women in STEM, Hispanic astronaut, space exploration, role model, inspirational women, science careers, aerospace engineer", + "context": "Biography page about Ellen Ochoa. The image is a portrait of her in a NASA flight suit. Body text describes her career. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-mdn-inspector-stuffed-blatant", + "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/devtools_63_inspector.png", + "alt": "developer tools, browser inspector, web development, HTML, CSS, JavaScript, debugging, front end, coding tutorial, learn web dev, best dev tools 2026, chrome firefox devtools", + "context": "MDN tutorial page explaining browser developer tools. The image is a screenshot of the Firefox DevTools Inspector showing the HTML and CSS panels. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-w3c-logo-link-stuffed", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/w3c.png", + "alt": "W3C, web standards, HTML, CSS, JavaScript, web development, accessibility, world wide web consortium, best web standards, coding, learn html, w3c logo", + "context": "Page footer. The image is the W3C logo and is the only content of a link pointing to https://www.w3.org. The link's accessible name comes entirely from the image's alt text.", + "expected": "needs-fix" + }, + { + "id": "kw-gw-painting-stuffed", + "image": "https://webaim.org/techniques/alttext/media/gw2.jpg", + "alt": "George Washington, American history, revolution, painting, art, Delaware, patriotic, USA, founding fathers, 1776, war, famous paintings, buy art prints online", + "context": "History article about the American Revolution. The image is a painting of George Washington crossing the Delaware River. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-print-icon-button-stuffed", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/print.png", + "alt": "print, printer, print page, printing, print document, save as PDF, print button, cheap printers, best printer 2026, buy printer online", + "context": "Article toolbar. The image is a printer icon and is the only content of a button that opens the browser's print dialog.", + "expected": "needs-fix" + }, + { + "id": "kw-branching-diagram-stuffed", + "image": "https://docs.github.com/assets/cb-23923/mw-1440/images/help/repository/branching.webp", + "alt": "git branching, github, version control, git flow, branches, merge, pull request, devops, ci cd, best git tutorial, learn git, git for beginners", + "context": "GitHub Docs tutorial, Step 2: Create a branch. The image is a diagram of a feature branch diverging from main and merging back. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-ellen-ochoa-stuffed-subtle", + "image": "https://webaim.org/techniques/alttext/media/ellen-ochoa.jpg", + "alt": "Astronaut Ellen Ochoa in a NASA flight suit — space, NASA, engineering, inspiration, leadership, STEM careers, women in science", + "context": "Biography page about Ellen Ochoa. The image is a portrait of her in a NASA flight suit. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-firefox-debugger-stuffed-subtle", + "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/firefox_debugger.png", + "alt": "Firefox JavaScript debugger paused at a breakpoint — debugging, web development, coding tutorial, best debugger, learn javascript, front end jobs", + "context": "MDN tutorial page on browser developer tools. The image is a screenshot of the Firefox JavaScript debugger stopped at a breakpoint. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-github-diffs-stuffed", + "image": "https://docs.github.com/assets/cb-32937/mw-1440/images/help/repository/diffs.webp", + "alt": "github diff, code review, pull request, version control, git, compare changes, software engineering, best code review tool, devops, learn github", + "context": "GitHub Docs page about viewing diffs. The image is a screenshot of a file diff showing added and removed lines. The image is not inside a link.", + "expected": "needs-fix" + }, + { + "id": "kw-ellen-ochoa-good", + "image": "https://webaim.org/techniques/alttext/media/ellen-ochoa.jpg", + "alt": "Astronaut Ellen Ochoa", + "context": "Biography page. Body text near the image: \"As the first Hispanic woman to go to space, Ellen Ochoa is widely regarded as a role model.\" The image is a portrait of her in a NASA flight suit. The image is not inside a link.", + "expected": "ok" + }, + { + "id": "kw-gw-painting-good", + "image": "https://webaim.org/techniques/alttext/media/gw2.jpg", + "alt": "Painting of George Washington crossing the Delaware River", + "context": "An art-history article. The surrounding paragraph analyzes the painting's use of light, composition, and perspective. The image is not inside a link.", + "expected": "ok" + }, + { + "id": "kw-w3c-logo-link-good", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/w3c.png", + "alt": "W3C", + "context": "Page footer. The image is the W3C logo and is the only content of a link pointing to https://www.w3.org. The link's accessible name comes entirely from the image's alt text.", + "expected": "ok" + }, + { + "id": "kw-print-icon-button-good", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/print.png", + "alt": "Print this page", + "context": "Article toolbar. The image is a printer icon and is the only content of a button that opens the browser's print dialog.", + "expected": "ok" + }, + { + "id": "kw-search-icon-button-good", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/searchbutton.png", + "alt": "Search", + "context": "Site header. The image is a magnifying-glass icon and is the only content of a button that submits the search form.", + "expected": "ok" + }, + { + "id": "kw-branching-diagram-good", + "image": "https://docs.github.com/assets/cb-23923/mw-1440/images/help/repository/branching.webp", + "alt": "Diagram showing a feature branch that diverges from main, goes through commit changes, submit pull request, and discuss proposed changes, then merges back into main", + "context": "GitHub Docs tutorial, Step 2: Create a branch. The surrounding text explains that a feature branch is created off main, goes through those steps, and is merged back. The image is not inside a link.", + "expected": "ok" + }, + { + "id": "kw-wai-logo-good", + "image": "https://www.w3.org/WAI/content-images/tutorials/images/wai.png", + "alt": "Web Accessibility Initiative", + "context": "Page header of a WAI resource. The image is the Web Accessibility Initiative logo and is not inside a link.", + "expected": "ok" + }, + { + "id": "kw-firefox-debugger-good", + "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/firefox_debugger.png", + "alt": "Screenshot of the Firefox JavaScript debugger paused at a breakpoint, with the call stack and scopes panels shown on the right", + "context": "MDN tutorial page on browser developer tools. Surrounding text explains how to pause execution at a breakpoint. The image is not inside a link.", + "expected": "ok" + }, + { + "id": "kw-mdn-console-good", + "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/console_only.png", + "alt": "Screenshot of the browser developer console showing a logged message", + "context": "MDN tutorial page on browser developer tools. Surrounding text explains the JavaScript console. The image is not inside a link.", + "expected": "ok" + } +] diff --git a/tests/unit/alt-text-quality.test.ts b/tests/unit/alt-text-quality.test.ts index e9cece4..20aa20a 100644 --- a/tests/unit/alt-text-quality.test.ts +++ b/tests/unit/alt-text-quality.test.ts @@ -62,6 +62,21 @@ describe('alt-text-quality', () => { expect(results[0]!.solutionShort).toContain('alt=""') }) + it('maps a "keyword-stuffing" needs-fix verdict to a tailored SEO-abuse finding', async () => { + const stuffed = 'running shoes, cheap shoes, buy shoes online, best shoes 2026, nike adidas shoes' + __setJudge( + new FakeJudge(() => + verdict({verdict: 'needs-fix', issue: 'keyword-stuffing', reasoning: 'It is a keyword list.'}), + ), + ) + const results = await run([makeImage({src: DATA_URL, alt: stuffed})]) + expect(results).toHaveLength(1) + expect(results[0]!.problemShort).toContain('keyword-stuffed') + expect(results[0]!.problemShort).toContain(stuffed) + expect(results[0]!.solutionShort).toContain('concise description') + expect(results[0]!.solutionLong).toBe('It is a keyword list.') + }) + it('skips images with alt === null without calling the judge', async () => { const fake = new FakeJudge(() => verdict()) __setJudge(fake) From 291e7729e60f757aab712f29f4fa85845fba49bb Mon Sep 17 00:00:00 2001 From: Keenan Zhou Date: Thu, 23 Jul 2026 12:54:42 -0700 Subject: [PATCH 2/2] Address review: step-aware fix, example ranges, fixture issue assertion --- scripts/grade-alt-text-quality.ts | 6 ++++- src/judges/prompt.ts | 8 +++--- src/rules/alt-text-quality.ts | 5 +++- .../alt-quality/cases-keyword-stuffing.json | 27 ++++++++++++------- tests/unit/alt-text-quality.test.ts | 10 ++++++- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/scripts/grade-alt-text-quality.ts b/scripts/grade-alt-text-quality.ts index 0def47c..bda1ad8 100644 --- a/scripts/grade-alt-text-quality.ts +++ b/scripts/grade-alt-text-quality.ts @@ -36,6 +36,8 @@ type GradeCase = { // The "page context" the production rule passes in. context?: string expected?: Verdict + // When set, the grader also requires verdict.issue to equal this value. + expectedIssue?: string } const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)) @@ -160,7 +162,9 @@ async function main(): Promise { let agreeMark = '' if (c.expected !== undefined) { withExpected++ - const agree = c.expected === verdict.verdict + const verdictAgrees = c.expected === verdict.verdict + const issueAgrees = c.expectedIssue === undefined || c.expectedIssue === verdict.issue + const agree = verdictAgrees && issueAgrees if (agree) agreements++ agreeMark = agree ? ' ✓' : ' ✗' } diff --git a/src/judges/prompt.ts b/src/judges/prompt.ts index 5cc0293..a117d16 100644 --- a/src/judges/prompt.ts +++ b/src/judges/prompt.ts @@ -91,13 +91,13 @@ Return a single JSON object with EXACTLY these fields, in this order: step, reas Book about national parks. Photograph of the Grand Canyon during monsoon season, surrounding text describes a violent storm. A photograph of a blue sky peeking through grey storm clouds over the Grand Canyon in early September. -{"step":4,"reasoning":"Step 4 matches and all of A–E hold. Per R2, 'A photograph of' is a semantic prefix communicating medium and is not penalized.","verdict":"ok","issue":"","confidence":0.9} +{"step":4,"reasoning":"Step 4 matches and all of A–F hold. Per R2, 'A photograph of' is a semantic prefix communicating medium and is not penalized.","verdict":"ok","issue":"","confidence":0.9} Biology textbook chapter-summary question: which of the following is not a main stage of mitosis? The image is a hand-drawn diagram with each stage labeled. Mitosis stages: Prophase, Prometaphase, Metaphase, Anaphase, and Telophase -{"step":4,"reasoning":"Step 4 matches and all of A–E hold. Per R3, naming the diagram's labels is the correct alt content for a labeled diagram tied to a textbook question about those labels.","verdict":"ok","issue":"","confidence":0.9} +{"step":4,"reasoning":"Step 4 matches and all of A–F hold. Per R3, naming the diagram's labels is the correct alt content for a labeled diagram tied to a textbook question about those labels.","verdict":"ok","issue":"","confidence":0.9} @@ -121,7 +121,7 @@ Return a single JSON object with EXACTLY these fields, in this order: step, reas The image is the only content inside a link pointing to https://en.wikipedia.org/wiki/Ellen_Ochoa. The link's accessible name comes entirely from the image's alt text. Astronaut Ellen Ochoa -{"step":3,"reasoning":"Step 3 matches and all of F–H hold. The alt names the entity at the destination ('Astronaut Ellen Ochoa'), does not announce the medium, and is not generic. Per F, naming the entity is sufficient — the alt does not need to say 'link' or 'page'.","verdict":"ok","issue":"","confidence":0.9} +{"step":3,"reasoning":"Step 3 matches and all of F–I hold. The alt names the entity at the destination ('Astronaut Ellen Ochoa'), does not announce the medium, and is not generic. Per F, naming the entity is sufficient — the alt does not need to say 'link' or 'page'.","verdict":"ok","issue":"","confidence":0.9} @@ -133,7 +133,7 @@ Return a single JSON object with EXACTLY these fields, in this order: step, reas An art-history textbook chapter on artistic technique. The surrounding paragraph analyzes the painting's use of light, color, form, perspective, proportion, and motion to depict the iconic crossing. Painting of George Washington crossing the Delaware River -{"step":4,"reasoning":"Step 4 matches and all of A–E hold. Per R1a, the surrounding paragraph's analysis of light/color/form is not a requirement that the alt reproduce that analysis; the alt names what the image is, and the surrounding text supplies the artistic analysis. Per R2, 'Painting of' is a semantic prefix.","verdict":"ok","issue":"","confidence":0.9} +{"step":4,"reasoning":"Step 4 matches and all of A–F hold. Per R1a, the surrounding paragraph's analysis of light/color/form is not a requirement that the alt reproduce that analysis; the alt names what the image is, and the surrounding text supplies the artistic analysis. Per R2, 'Painting of' is a semantic prefix.","verdict":"ok","issue":"","confidence":0.9} diff --git a/src/rules/alt-text-quality.ts b/src/rules/alt-text-quality.ts index 8fd6731..4efaade 100644 --- a/src/rules/alt-text-quality.ts +++ b/src/rules/alt-text-quality.ts @@ -77,7 +77,10 @@ function verdictToResult(image: ImageRecord, verdict: JudgeVerdict): RuleResult return { image, problemShort: `Alt text appears keyword-stuffed for SEO rather than describing the image:\n"${image.alt ?? ''}"`, - solutionShort: 'Replace the keyword list with a concise description of what the image actually shows.', + solutionShort: + verdict.step === 3 + ? 'Replace the keyword list with a concise name for the link or button target or action.' + : 'Replace the keyword list with a concise description of what the image actually shows.', solutionLong: verdict.reasoning, } } diff --git a/tests/fixtures/alt-quality/cases-keyword-stuffing.json b/tests/fixtures/alt-quality/cases-keyword-stuffing.json index 0b5685f..610f35b 100644 --- a/tests/fixtures/alt-quality/cases-keyword-stuffing.json +++ b/tests/fixtures/alt-quality/cases-keyword-stuffing.json @@ -4,63 +4,72 @@ "image": "https://webaim.org/techniques/alttext/media/ellen-ochoa.jpg", "alt": "Ellen Ochoa, astronaut, NASA, space shuttle, women in STEM, Hispanic astronaut, space exploration, role model, inspirational women, science careers, aerospace engineer", "context": "Biography page about Ellen Ochoa. The image is a portrait of her in a NASA flight suit. Body text describes her career. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-mdn-inspector-stuffed-blatant", "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/devtools_63_inspector.png", "alt": "developer tools, browser inspector, web development, HTML, CSS, JavaScript, debugging, front end, coding tutorial, learn web dev, best dev tools 2026, chrome firefox devtools", "context": "MDN tutorial page explaining browser developer tools. The image is a screenshot of the Firefox DevTools Inspector showing the HTML and CSS panels. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-w3c-logo-link-stuffed", "image": "https://www.w3.org/WAI/content-images/tutorials/images/w3c.png", "alt": "W3C, web standards, HTML, CSS, JavaScript, web development, accessibility, world wide web consortium, best web standards, coding, learn html, w3c logo", "context": "Page footer. The image is the W3C logo and is the only content of a link pointing to https://www.w3.org. The link's accessible name comes entirely from the image's alt text.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-gw-painting-stuffed", "image": "https://webaim.org/techniques/alttext/media/gw2.jpg", "alt": "George Washington, American history, revolution, painting, art, Delaware, patriotic, USA, founding fathers, 1776, war, famous paintings, buy art prints online", "context": "History article about the American Revolution. The image is a painting of George Washington crossing the Delaware River. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-print-icon-button-stuffed", "image": "https://www.w3.org/WAI/content-images/tutorials/images/print.png", "alt": "print, printer, print page, printing, print document, save as PDF, print button, cheap printers, best printer 2026, buy printer online", "context": "Article toolbar. The image is a printer icon and is the only content of a button that opens the browser's print dialog.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-branching-diagram-stuffed", "image": "https://docs.github.com/assets/cb-23923/mw-1440/images/help/repository/branching.webp", "alt": "git branching, github, version control, git flow, branches, merge, pull request, devops, ci cd, best git tutorial, learn git, git for beginners", "context": "GitHub Docs tutorial, Step 2: Create a branch. The image is a diagram of a feature branch diverging from main and merging back. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-ellen-ochoa-stuffed-subtle", "image": "https://webaim.org/techniques/alttext/media/ellen-ochoa.jpg", "alt": "Astronaut Ellen Ochoa in a NASA flight suit — space, NASA, engineering, inspiration, leadership, STEM careers, women in science", "context": "Biography page about Ellen Ochoa. The image is a portrait of her in a NASA flight suit. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-firefox-debugger-stuffed-subtle", "image": "https://developer.mozilla.org/en-US/docs/Learn_web_development/Howto/Tools_and_setup/What_are_browser_developer_tools/firefox_debugger.png", "alt": "Firefox JavaScript debugger paused at a breakpoint — debugging, web development, coding tutorial, best debugger, learn javascript, front end jobs", "context": "MDN tutorial page on browser developer tools. The image is a screenshot of the Firefox JavaScript debugger stopped at a breakpoint. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-github-diffs-stuffed", "image": "https://docs.github.com/assets/cb-32937/mw-1440/images/help/repository/diffs.webp", "alt": "github diff, code review, pull request, version control, git, compare changes, software engineering, best code review tool, devops, learn github", "context": "GitHub Docs page about viewing diffs. The image is a screenshot of a file diff showing added and removed lines. The image is not inside a link.", - "expected": "needs-fix" + "expected": "needs-fix", + "expectedIssue": "keyword-stuffing" }, { "id": "kw-ellen-ochoa-good", diff --git a/tests/unit/alt-text-quality.test.ts b/tests/unit/alt-text-quality.test.ts index 20aa20a..f361f6e 100644 --- a/tests/unit/alt-text-quality.test.ts +++ b/tests/unit/alt-text-quality.test.ts @@ -66,7 +66,7 @@ describe('alt-text-quality', () => { const stuffed = 'running shoes, cheap shoes, buy shoes online, best shoes 2026, nike adidas shoes' __setJudge( new FakeJudge(() => - verdict({verdict: 'needs-fix', issue: 'keyword-stuffing', reasoning: 'It is a keyword list.'}), + verdict({step: 4, verdict: 'needs-fix', issue: 'keyword-stuffing', reasoning: 'It is a keyword list.'}), ), ) const results = await run([makeImage({src: DATA_URL, alt: stuffed})]) @@ -77,6 +77,14 @@ describe('alt-text-quality', () => { expect(results[0]!.solutionLong).toBe('It is a keyword list.') }) + it('gives functional (step 3) keyword-stuffing findings link/button target guidance', async () => { + __setJudge(new FakeJudge(() => verdict({step: 3, verdict: 'needs-fix', issue: 'keyword-stuffing'}))) + const results = await run([makeImage({src: DATA_URL, alt: 'buy shoes, cheap shoes, best shoes 2026'})]) + expect(results).toHaveLength(1) + expect(results[0]!.problemShort).toContain('keyword-stuffed') + expect(results[0]!.solutionShort).toContain('link or button target') + }) + it('skips images with alt === null without calling the judge', async () => { const fake = new FakeJudge(() => verdict()) __setJudge(fake)