diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..ca0246c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + JAVA_VERSION: 21 + +jobs: + test: + strategy: + fail-fast: false + matrix: + node-version: ['22.x'] + os: [ubuntu-latest, windows-2022, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + registry-url: 'https://registry.npmjs.org/' + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm install + + - name: Build the project + run: npm run build + + - name: Run tests + run: npm run test diff --git a/jest.config.js b/jest.config.js index e142053..8ccda6d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,6 @@ -import { weaverConfig } from "@specs-feup/clava/code/WeaverConfiguration.js"; - const config = { preset: "ts-jest/presets/default-esm", - testEnvironment: "@specs-feup/lara/jest/jestEnvironment.js", - testEnvironmentOptions: { - weaverConfig, - }, - globalSetup: "@specs-feup/lara/jest/jestGlobalSetup.js", - globalTeardown: "@specs-feup/lara/jest/jestGlobalTeardown.js", - setupFiles: ["@specs-feup/lara/jest/setupFiles/sharedJavaModule.js"], + testEnvironment: "node", //notify: true, //notifyMode: "always", //verbose: true, diff --git a/package.json b/package.json index 696d8c8..d91b6d8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test:watch": "npm run test -- --watch" }, "dependencies": { - "@specs-feup/lara": "^3.0.4", + "@specs-feup/lara": "^3.5.2", "express": "^4.21.2", "ws": "^8.18.0" }, diff --git a/src/AstConverterUtils.test.ts b/src/AstConverterUtils.test.ts new file mode 100644 index 0000000..0e5e8dc --- /dev/null +++ b/src/AstConverterUtils.test.ts @@ -0,0 +1,60 @@ +import { + addIdentation, + escapeHtml, + getNodeCodeTags, + getSpanTags, + getSyntaxHighlightTags, + normalizeFilepath, +} from "./AstConverterUtils.js"; + +describe("AstConverterUtils", () => { + describe("addIdentation", () => { + it("indents every line except the first one", () => { + expect(addIdentation("first\nsecond\nthird", 2)).toBe( + "first\n second\n third" + ); + }); + + it("leaves single-line code unchanged", () => { + expect(addIdentation("const value = 1;", 3)).toBe("const value = 1;"); + }); + }); + + it("escapes HTML control characters without changing other text", () => { + expect(escapeHtml("if (left < right && right > 0)")) + .toBe("if (left < right && right > 0)"); + }); + + it("creates generic span tags with all supplied attributes", () => { + expect(getSpanTags('class="node"', 'data-id="42"')).toEqual([ + '', + "", + ]); + }); + + it("creates node-code span tags", () => { + expect(getNodeCodeTags("node-7")).toEqual([ + '', + "", + ]); + }); + + it.each(["comment", "keyword", "literal", "string", "type"] as const)( + "creates %s syntax-highlighting tags", + (type) => { + expect(getSyntaxHighlightTags(type)).toEqual([ + ``, + "", + ]); + } + ); + + it("normalizes Windows separators and preserves POSIX separators", () => { + expect(normalizeFilepath("src\\public\\index.html")).toBe( + "src/public/index.html" + ); + expect(normalizeFilepath("src/public/index.html")).toBe( + "src/public/index.html" + ); + }); +}); diff --git a/src/public/js/ToolJoinPoint.test.ts b/src/public/js/ToolJoinPoint.test.ts new file mode 100644 index 0000000..51c5851 --- /dev/null +++ b/src/public/js/ToolJoinPoint.test.ts @@ -0,0 +1,69 @@ +import ToolJoinPoint from "./ToolJoinPoint.js"; + +describe("ToolJoinPoint", () => { + const jsonTree = { + id: "root", + type: "program", + code: "const answer = 42;", + filepath: "src/example.ts", + info: { language: "TypeScript" }, + children: [ + { + id: "child", + type: "declaration", + code: "const answer = 42;", + filepath: "src/example.ts", + info: { name: "answer" }, + children: [], + }, + ], + }; + + it("builds a nested join-point tree from JSON", () => { + const root = ToolJoinPoint.fromJson(jsonTree); + + expect(root.id).toBe("root"); + expect(root.type).toBe("program"); + expect(root.code).toBe("const answer = 42;"); + expect(root.filepath).toBe("src/example.ts"); + expect(root.info).toEqual({ language: "TypeScript" }); + expect(root.children).toHaveLength(1); + expect(root.children[0]).toBeInstanceOf(ToolJoinPoint); + expect(root.children[0].id).toBe("child"); + }); + + it("serializes a nested join-point tree to JSON", () => { + expect(ToolJoinPoint.fromJson(jsonTree).toJson()).toEqual(jsonTree); + }); + + it("clones the complete tree into new join-point instances", () => { + const original = ToolJoinPoint.fromJson(jsonTree); + const clone = original.clone(); + + expect(clone).not.toBe(original); + expect(clone.children[0]).not.toBe(original.children[0]); + expect(clone.toJson()).toEqual(original.toJson()); + }); + + it("supports join points without code or a filepath", () => { + const joinPoint = new ToolJoinPoint( + "synthetic", + "scope", + undefined, + undefined, + {}, + [] + ); + + expect(joinPoint.code).toBeUndefined(); + expect(joinPoint.filepath).toBeUndefined(); + expect(joinPoint.toJson()).toEqual({ + id: "synthetic", + type: "scope", + code: undefined, + filepath: undefined, + info: {}, + children: [], + }); + }); +}); diff --git a/src/public/js/utils.test.ts b/src/public/js/utils.test.ts new file mode 100644 index 0000000..fd24985 --- /dev/null +++ b/src/public/js/utils.test.ts @@ -0,0 +1,15 @@ +import { countChar } from "./utils.js"; + +describe("countChar", () => { + it("counts every occurrence of the requested character", () => { + expect(countChar("abracadabra", "a")).toBe(5); + }); + + it("returns zero when the character is absent", () => { + expect(countChar("visualization", "x")).toBe(0); + }); + + it("handles Unicode characters", () => { + expect(countChar("a✨b✨c", "✨")).toBe(2); + }); +});