diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc3b792d5..849105bb0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: "Test build" +name: "Test and Build" on: push: @@ -18,6 +18,27 @@ env: NODE_VERSION: 18 jobs: + test: + name: Run tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + submodules: recursive + + - name: Set up Node.js ${{ env.NODE_VERSION }} + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + cache: yarn + node-version: ${{ env.NODE_VERSION }} + + - name: Install dependencies + run: script/bootstrap + + - name: Run tests + run: npx vitest run + build_frontend: name: Test build runs-on: ubuntu-latest diff --git a/src/tools/markdown.ts b/src/tools/markdown.ts index 029a34811..5a6cf8683 100644 --- a/src/tools/markdown.ts +++ b/src/tools/markdown.ts @@ -31,12 +31,29 @@ export const markdownWithRepositoryContext = (input: string, repository?: Reposi return x.replace("(#", `(/hacs/repository/${repository.id}#`); }); - // Add references to issues and PRs - input = input.replace(/(?:\w[\w-.]+\/\w[\w-.]+|\B)#[1-9]\d*\b/g, (reference) => { - const fullReference = reference.replace(/^#/, `${repository.full_name}#`); - const [fullName, issue] = fullReference.split("#"); - return `[${reference}](https://github.com/${fullName}/issues/${issue})`; - }); + // Add references to issues and PRs (avoid CSS hex colors and code blocks) + input = input.replace( + /(^|[\s])((?:\w[\w-.]+\/\w[\w-.]+)?#[1-9]\d*)\b/g, + (match, prefix, reference, offset) => { + const issueNumber = reference.split("#")[1]; + + // Skip if it's a valid CSS hex color (only contains 0-9a-f and is 3 or 6 digits) + if (issueNumber && /^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$/.test(issueNumber)) { + // Check if it's in a CSS context + if (/(?<=color:\s*|background:\s*|border:\s*|:\s*)$/.test(input.substring(0, offset))) { + return match; + } + } + + const fullReference = reference.includes("/") + ? reference + : `${repository.full_name}${reference}`; + const [fullName, issue] = fullReference.split("#"); + return fullName && issue + ? `${prefix}[${reference}](https://github.com/${fullName}/issues/${issue})` + : match; + }, + ); } return input; }; diff --git a/tests/markdown.test.ts b/tests/markdown.test.ts new file mode 100644 index 000000000..8afe34038 --- /dev/null +++ b/tests/markdown.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect } from "vitest"; +import { markdownWithRepositoryContext } from "../src/tools/markdown.js"; +import type { RepositoryInfo } from "../src/data/repository.js"; + +const mockRepository: RepositoryInfo = { + id: "123", + full_name: "awesome/repo", + default_branch: "main", + available_version: "v1.0.0", +} as RepositoryInfo; + +describe("markdownWithRepositoryContext", () => { + it("should convert GitHub issue references to links", () => { + const input = "See issue #123 for details"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe("See issue [#123](https://github.com/awesome/repo/issues/123) for details"); + }); + + it("should NOT convert CSS color codes", () => { + const input = "Set color: #123456 in your CSS"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe("Set color: #123456 in your CSS"); + }); + + it("should NOT convert CSS shorthand colors", () => { + const input = "Use background:#123; border:#abc;"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe("Use background:#123; border:#abc;"); + }); + + it("should handle mixed scenarios correctly", () => { + const input = "Fix issue #42 but keep color:#333 and border:#456 styles intact"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Fix issue [#42](https://github.com/awesome/repo/issues/42) but keep color:#333 and border:#456 styles intact", + ); + }); + + it("should handle duplicate patterns correctly", () => { + const input = "Issue #123 was fixed. Later, set color: #123 in CSS. Then see #456."; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Issue [#123](https://github.com/awesome/repo/issues/123) was fixed. Later, set color: #123 in CSS. Then see [#456](https://github.com/awesome/repo/issues/456).", + ); + }); + + it("should convert legitimate issue numbers but not hex colors", () => { + const input = "Issue #123 was fixed, but color: #456789 remains unchanged"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Issue [#123](https://github.com/awesome/repo/issues/123) was fixed, but color: #456789 remains unchanged", + ); + }); + + it("should work without repository context", () => { + const input = "Some text with #123 reference"; + const result = markdownWithRepositoryContext(input); + expect(result).toBe("Some text with #123 reference"); + }); + + it("should NOT convert CSS hex colors in code examples", () => { + const input = "Example CSS: div { color:#ff0000; background:#123456; }"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe("Example CSS: div { color:#ff0000; background:#123456; }"); + }); + + it("should handle repository/issue references correctly", () => { + const input = "Check out awesome/other-repo#456 for more info"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Check out [awesome/other-repo#456](https://github.com/awesome/other-repo/issues/456) for more info", + ); + }); + + it("should handle issue numbers with 8 and 9 (non-hex digits)", () => { + const input = "See issues #789 and #98 for details"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "See issues [#789](https://github.com/awesome/repo/issues/789) and [#98](https://github.com/awesome/repo/issues/98) for details", + ); + }); + + it("should NOT convert 6-digit hex colors in CSS context", () => { + const input = "Apply color:#ffffff and background-color:#000000 styles"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe("Apply color:#ffffff and background-color:#000000 styles"); + }); + + it("should convert valid issue numbers that contain non-hex digits", () => { + const input = "Fixed in #987 and resolved in #1289"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Fixed in [#987](https://github.com/awesome/repo/issues/987) and resolved in [#1289](https://github.com/awesome/repo/issues/1289)", + ); + }); + + it("should handle mixed CSS and issue references in same text", () => { + const input = "Issue #789 fixed. Use border:#123; and see #890 too"; + const result = markdownWithRepositoryContext(input, mockRepository); + expect(result).toBe( + "Issue [#789](https://github.com/awesome/repo/issues/789) fixed. Use border:#123; and see [#890](https://github.com/awesome/repo/issues/890) too", + ); + }); + it("should handle large text efficiently", () => { + // Create a large string with mixed content to test performance + const parts: string[] = []; + for (let i = 0; i < 25_000; i++) { + parts.push(`Issue #${i + 1} was fixed. Use color:#${String(i).padStart(3, "0")}; in CSS.`); + } + const input = parts.join(" "); + + const start = performance.now(); + const result = markdownWithRepositoryContext(input, mockRepository); + const end = performance.now(); + + // Should complete in reasonable time (< 100ms for this test) + expect(end - start).toBeLessThan(100); + + // Should have converted all issue references + expect(result).toContain("[#1](https://github.com/awesome/repo/issues/1)"); + expect(result).toContain("[#25000](https://github.com/awesome/repo/issues/25000)"); + + // Should not have converted CSS colors + expect(result).toContain("color:#001;"); + expect(result).toContain("color:#099;"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..05a242924 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.{js,ts}"], + }, +});