From 695ca8188e6c6a21b3a1b159b314ec06d0bf57b9 Mon Sep 17 00:00:00 2001 From: Rivka Ungar Date: Mon, 9 Feb 2026 19:55:12 +0200 Subject: [PATCH 1/2] chore: add tests for a11y and example scripts --- .../__tests__/extract-accessibility.test.js | 147 ++++++++++++++++++ .../__tests__/extract-code-samples.test.js | 98 ++++++++++++ packages/mcp/scripts/extract-accessibility.js | 20 +-- packages/mcp/scripts/extract-code-samples.js | 44 +++--- packages/mcp/vitest.config.ts | 10 ++ 5 files changed, 290 insertions(+), 29 deletions(-) create mode 100644 packages/mcp/scripts/__tests__/extract-accessibility.test.js create mode 100644 packages/mcp/scripts/__tests__/extract-code-samples.test.js create mode 100644 packages/mcp/vitest.config.ts diff --git a/packages/mcp/scripts/__tests__/extract-accessibility.test.js b/packages/mcp/scripts/__tests__/extract-accessibility.test.js new file mode 100644 index 0000000000..282a5e26a5 --- /dev/null +++ b/packages/mcp/scripts/__tests__/extract-accessibility.test.js @@ -0,0 +1,147 @@ +import { describe, it, expect } from "vitest"; +import { cleanUpAccessibilityContent } from "../extract-accessibility.js"; + +describe("cleanUpAccessibilityContent", () => { + describe("UsageGuidelines extraction", () => { + it("should extract numbered guidelines from quoted strings", () => { + const content = ``; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe( + "1. Use labels for all interactive elements\n2. Ensure color contrast meets WCAG AA" + ); + }); + + it("should extract guidelines wrapped in React fragments", () => { + const content = `First guideline text, + <>Second guideline text + ]} />`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe("1. First guideline text\n2. Second guideline text"); + }); + + it("should convert tags to backticks within guidelines", () => { + const content = `Use the aria-label prop for accessibility + ]} />`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toContain("`aria-label`"); + expect(result).not.toContain(""); + expect(result).not.toContain(""); + }); + + it("should handle mixed quoted strings and React fragments", () => { + const content = `Fragment with code inside + ]} />`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toContain("1. Simple string guideline"); + expect(result).toContain("2. Fragment with `code` inside"); + }); + + it("should number guidelines sequentially", () => { + const content = ``; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe("1. First\n2. Second\n3. Third"); + }); + + it("should skip whitespace-only React fragment guidelines", () => { + const content = `Valid guideline, + <> , + <>Another valid guideline + ]} />`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe("1. Valid guideline\n2. Another valid guideline"); + }); + }); + + describe("fallback cleanup", () => { + it("should remove UsageGuidelines JSX components", () => { + const content = `Some text before\n\nSome text after`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).not.toContain("UsageGuidelines"); + expect(result).toContain("Some text before"); + expect(result).toContain("Some text after"); + }); + + it("should replace tags with backticks in fallback mode", () => { + const content = `Use the role attribute for elements.`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe("Use the `role` attribute for elements."); + }); + + it("should remove React fragment tags in fallback mode", () => { + const content = `<>Some content inside fragments`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).toBe("Some content inside fragments"); + }); + + it("should remove lines with only brackets or punctuation", () => { + const content = `Valid content\n [\n ]\nMore valid content`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).not.toMatch(/^\s*[\[\]]\s*$/m); + expect(result).toContain("Valid content"); + expect(result).toContain("More valid content"); + }); + + it("should reduce multiple empty lines to double newlines", () => { + const content = `First paragraph\n\n\n\nSecond paragraph`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).not.toContain("\n\n\n"); + expect(result).toContain("First paragraph\n\nSecond paragraph"); + }); + + it("should return fallback message for empty content", () => { + const result = cleanUpAccessibilityContent(""); + + expect(result).toBe("No accessibility guidelines found in expected format."); + }); + + it("should return fallback message for whitespace-only content", () => { + const result = cleanUpAccessibilityContent(" \n \n "); + + expect(result).toBe("No accessibility guidelines found in expected format."); + }); + + it("should remove guidelines= attribute syntax in fallback mode", () => { + const content = `Some preamble\nguidelines={something}\nAfter text`; + + const result = cleanUpAccessibilityContent(content); + + expect(result).not.toContain("guidelines="); + expect(result).toContain("Some preamble"); + expect(result).toContain("After text"); + }); + }); +}); diff --git a/packages/mcp/scripts/__tests__/extract-code-samples.test.js b/packages/mcp/scripts/__tests__/extract-code-samples.test.js new file mode 100644 index 0000000000..a9473ad0e9 --- /dev/null +++ b/packages/mcp/scripts/__tests__/extract-code-samples.test.js @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import parser from "@babel/parser"; +import { generateCodeForOneLiner } from "../extract-code-samples.js"; + +function parseCode(code) { + return parser.parse(code, { + sourceType: "module", + plugins: ["typescript", "jsx"] + }); +} + +describe("generateCodeForOneLiner", () => { + it("should return generated code for a regular variable declaration", () => { + const code = `const myComponent = () =>
Hello
;`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "myComponent"); + + expect(result).not.toBeNull(); + expect(result).toContain("const"); + expect(result).toContain("myComponent"); + }); + + it("should return empty string for a createComponentTemplate call", () => { + const code = `const myTemplate = createComponentTemplate(SomeComponent);`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "myTemplate"); + + expect(result).toBe(""); + }); + + it("should return null when the variable name is not found", () => { + const code = `const other = 42;`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "nonExistent"); + + expect(result).toBeNull(); + }); + + it("should handle multiple variable declarations and find the correct one", () => { + const code = ` + const first = createComponentTemplate(A); + const second = () => Test; + const third = 42; + `; + const ast = parseCode(code); + + expect(generateCodeForOneLiner(ast, "first")).toBe(""); + expect(generateCodeForOneLiner(ast, "second")).toContain("second"); + expect(generateCodeForOneLiner(ast, "third")).toContain("third"); + }); + + it("should prepend 'const ' to the generated code", () => { + const code = `const myVar = "hello";`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "myVar"); + + expect(result).toMatch(/^const /); + }); +}); + +describe("generateCodeForOneLiner - JSX handling", () => { + it("should generate code for a variable with JSX value", () => { + const code = `const myElement =
Hello
;`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "myElement"); + + expect(result).toContain("myElement"); + expect(result).toContain("div"); + expect(result).toContain("wrapper"); + }); + + it("should generate code for an arrow function returning JSX", () => { + const code = `const MyComponent = () => ;`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "MyComponent"); + + expect(result).not.toBeNull(); + expect(result).toContain("MyComponent"); + expect(result).toContain("Button"); + }); + + it("should handle a variable with a nested function call (not createComponentTemplate)", () => { + const code = `const myVar = someOtherFunction(Arg1, Arg2);`; + const ast = parseCode(code); + + const result = generateCodeForOneLiner(ast, "myVar"); + + expect(result).not.toBeNull(); + expect(result).toContain("const"); + expect(result).toContain("someOtherFunction"); + }); +}); diff --git a/packages/mcp/scripts/extract-accessibility.js b/packages/mcp/scripts/extract-accessibility.js index 42ed5b73f5..aeafe0e8a5 100644 --- a/packages/mcp/scripts/extract-accessibility.js +++ b/packages/mcp/scripts/extract-accessibility.js @@ -7,11 +7,7 @@ const __dirname = path.dirname(__filename); const componentsDir = path.join(__dirname, "../../docs/src/pages/components/"); const outputDir = path.join(__dirname, "../dist/generated/accessibility/"); -if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); -} - -function getMdxFiles() { +export function getMdxFiles() { const mdxFiles = []; function traverseDirectory(dir) { @@ -36,7 +32,7 @@ function getMdxFiles() { return mdxFiles; } -function extractAccessibilityFromMdx(file) { +export function extractAccessibilityFromMdx(file) { const inputFile = path.join(__dirname, file); const inputFileComponentName = path.basename(inputFile).split(".")[0]; const outputFile = path.resolve(__dirname, outputDir, inputFileComponentName + ".md"); @@ -91,7 +87,7 @@ function extractAccessibilityFromMdx(file) { console.log(`Accessibility extracted for ${inputFileComponentName} at ${outputFile}`); } -function cleanUpAccessibilityContent(content) { +export function cleanUpAccessibilityContent(content) { // Extract guidelines from UsageGuidelines component const guidelinesMatch = content.match(/guidelines=\{(\[[\s\S]*?\])\}/); @@ -143,7 +139,11 @@ function cleanUpAccessibilityContent(content) { return cleaned || "No accessibility guidelines found in expected format."; } -function run() { +export function run() { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + console.log("🔍 Extracting accessibility content from MDX files..."); const mdxFiles = getMdxFiles(); @@ -162,4 +162,6 @@ function run() { console.log("✅ Accessibility extraction completed!"); } -run(); +if (process.argv[1] === __filename) { + run(); +} diff --git a/packages/mcp/scripts/extract-code-samples.js b/packages/mcp/scripts/extract-code-samples.js index 2c656ef8af..3238eab00b 100644 --- a/packages/mcp/scripts/extract-code-samples.js +++ b/packages/mcp/scripts/extract-code-samples.js @@ -1,19 +1,19 @@ import fs from "fs"; import path from "path"; import parser from "@babel/parser"; -import traverse from "@babel/traverse"; -import generate from "@babel/generator"; +import _traverse from "@babel/traverse"; +import _generate from "@babel/generator"; + +const traverse = _traverse.default ?? _traverse; +const generate = _generate.default ?? _generate; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const componentsDir = path.join(__dirname, "../../docs/src/pages/components/"); const outputDir = path.join(__dirname, "../dist/generated/"); -if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); -} -function getStoryFiles() { +export function getStoryFiles() { const storyFiles = []; function traverseDirectory(dir) { @@ -38,15 +38,13 @@ function getStoryFiles() { return storyFiles; } -const files = getStoryFiles(); - -function generateCodeForOneLiner(ast, constName) { +export function generateCodeForOneLiner(ast, constName) { let calleeCode = null; - traverse.default(ast, { + traverse(ast, { VariableDeclarator(path) { if (path.node.id.name === constName) { if (path.node.init?.callee?.name !== "createComponentTemplate") { - calleeCode = "const " + generate.default(path.node).code; + calleeCode = "const " + generate(path.node).code; } else { return (calleeCode = ""); } @@ -56,7 +54,7 @@ function generateCodeForOneLiner(ast, constName) { return calleeCode; } -function extractMarkdown(file) { +export function extractMarkdown(file) { const inputFile = path.join(__dirname, file); const inputFileComponentName = path.basename(inputFile).split(".")[0]; const outputFile = path.resolve(__dirname, outputDir, inputFileComponentName + ".md"); @@ -74,17 +72,17 @@ function extractMarkdown(file) { let markdown = "# Storybook Code Examples\n\n"; let accordionTemplateCode = ""; - traverse.default(ast, { + traverse(ast, { VariableDeclarator(path) { // console.log(path.node); if (path.node.id.name === "accordionTemplate" && path.node.init.type === "ArrowFunctionExpression") { - accordionTemplateCode = generate.default(path.node.init).code; + accordionTemplateCode = generate(path.node.init).code; } } }); // Traverse the AST to find all exported story objects - traverse.default(ast, { + traverse(ast, { ExportNamedDeclaration(path) { if (path.node.declaration && path.node.declaration.type === "VariableDeclaration") { path.node.declaration.declarations.forEach(declarator => { @@ -113,12 +111,12 @@ function extractMarkdown(file) { renderProp.body.type == "MemberExpression" || renderProp.body.type == "JSXElement" ) { - codeBlock = generate.default(renderProp.body).code; + codeBlock = generate(renderProp.body).code; } else if (renderProp.body.type == "BlockStatement") { - codeBlock = renderProp.body.body.map(line => generate.default(line).code).join("\n"); + codeBlock = renderProp.body.body.map(line => generate(line).code).join("\n"); } else { console.log(`${nameProp || storyName} is a ${renderProp.body.type}`); - codeBlock = generate.default(renderProp.body).code; + codeBlock = generate(renderProp.body).code; } } else if (renderProp.type == "CallExpression") { // console.log(renderProp.callee.object.name); @@ -142,10 +140,16 @@ function extractMarkdown(file) { console.log(`Code samples generated for ${inputFileComponentName} at ${outputFile}`); } -function run() { +export function run() { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); + } + const files = getStoryFiles(); files.forEach(file => { extractMarkdown(file); }); } -run(); +if (process.argv[1] === __filename) { + run(); +} diff --git a/packages/mcp/vitest.config.ts b/packages/mcp/vitest.config.ts new file mode 100644 index 0000000000..105034bbab --- /dev/null +++ b/packages/mcp/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["**/__tests__/**/*.test.js"], + clearMocks: true + } +}); From 4aae4e2abca8d1dda9b92f1e21088d5ac1936984 Mon Sep 17 00:00:00 2001 From: Rivka Ungar Date: Mon, 9 Feb 2026 20:00:08 +0200 Subject: [PATCH 2/2] add tests --- .../__tests__/extract-accessibility.test.js | 17 ++++++++++++++++- .../__tests__/extract-code-samples.test.js | 17 ++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/mcp/scripts/__tests__/extract-accessibility.test.js b/packages/mcp/scripts/__tests__/extract-accessibility.test.js index 282a5e26a5..7cf65a16c5 100644 --- a/packages/mcp/scripts/__tests__/extract-accessibility.test.js +++ b/packages/mcp/scripts/__tests__/extract-accessibility.test.js @@ -1,5 +1,11 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; import { describe, it, expect } from "vitest"; -import { cleanUpAccessibilityContent } from "../extract-accessibility.js"; +import { cleanUpAccessibilityContent, run } from "../extract-accessibility.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outputDir = path.join(__dirname, "../../dist/generated/accessibility/"); describe("cleanUpAccessibilityContent", () => { describe("UsageGuidelines extraction", () => { @@ -145,3 +151,12 @@ describe("cleanUpAccessibilityContent", () => { }); }); }); + +describe("run - integration", () => { + it("should generate accessibility markdown files from MDX files", () => { + run(); + + const outputFiles = fs.readdirSync(outputDir).filter(f => f.endsWith(".md")); + expect(outputFiles.length).toBeGreaterThan(0); + }); +}); diff --git a/packages/mcp/scripts/__tests__/extract-code-samples.test.js b/packages/mcp/scripts/__tests__/extract-code-samples.test.js index a9473ad0e9..54b1f00a56 100644 --- a/packages/mcp/scripts/__tests__/extract-code-samples.test.js +++ b/packages/mcp/scripts/__tests__/extract-code-samples.test.js @@ -1,6 +1,12 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; import { describe, it, expect } from "vitest"; import parser from "@babel/parser"; -import { generateCodeForOneLiner } from "../extract-code-samples.js"; +import { generateCodeForOneLiner, run } from "../extract-code-samples.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const outputDir = path.join(__dirname, "../../dist/generated/"); function parseCode(code) { return parser.parse(code, { @@ -96,3 +102,12 @@ describe("generateCodeForOneLiner - JSX handling", () => { expect(result).toContain("someOtherFunction"); }); }); + +describe("run - integration", () => { + it("should generate markdown files from story files", () => { + run(); + + const outputFiles = fs.readdirSync(outputDir).filter(f => f.endsWith(".md")); + expect(outputFiles.length).toBeGreaterThan(0); + }); +});