diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..656f6a5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,38 @@ +name: CI - PR Tests + +on: + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Start Vite dev server + run: | + nohup npm run dev:ci > vite.log 2>&1 & + npx wait-on http://localhost:5173 + env: + CI: true + + - name: Run Puppeteer tests (test:ci) + run: npm run test:ci + env: + CI: true + + - name: Display coverage + run: | + npm run collect:coverage:text diff --git a/package-lock.json b/package-lock.json index 801cfb4..28061b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.4", "tw-animate-css": "^1.2.5", + "twd-js": "^0.8.0", "zustand": "^5.0.3" }, "devDependencies": { @@ -47,7 +48,6 @@ "npm-run-all": "^4.1.5", "nyc": "^17.1.0", "puppeteer": "^24.29.1", - "twd-js": "^0.8.0", "typescript": "~5.7.2", "typescript-eslint": "^8.26.1", "vite": "^6.3.1", @@ -8374,9 +8374,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "dev": true, "license": "MIT", "engines": { @@ -13180,7 +13180,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/twd-js/-/twd-js-0.8.0.tgz", "integrity": "sha512-XdwGp4lH7islefN02ExS9R64Tw5MQDI4V2H1bxdyWNPgz5BG3WLey7Jew8kLVoRrtISunV3E849yIMFlHG+eog==", - "dev": true, "license": "MIT", "dependencies": { "chalk": "^5.6.2" @@ -13200,7 +13199,6 @@ "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" diff --git a/package.json b/package.json index 07f3167..4a52811 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,17 @@ "type": "module", "scripts": { "dev": "vite", + "dev:ci": "CI=true VITE_COVERAGE=true vite", "build": "tsc -b && vite build", "lint": "eslint .", "test": "vitest", "preview": "vite preview", "serve": "json-server --watch data/data.json --routes data/routes.json --port 3001", "serve:dev": "npm-run-all --parallel serve dev", - "collect:coverage": "nyc report --reporter=html --report-dir=coverage --temp-dir=coverage", - "test:ci": "node scripts/run-tests-ci.js" + "test:ci": "node scripts/run-tests-ci.js", + "collect:coverage:html": "npx nyc report --reporter=html --report-dir=coverage", + "collect:coverage:lcov": "npx nyc report --reporter=lcov --report-dir=coverage", + "collect:coverage:text": "npx nyc report --reporter=text --report-dir=coverage" }, "dependencies": { "@radix-ui/react-avatar": "^1.1.4", @@ -32,6 +35,7 @@ "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.4", "tw-animate-css": "^1.2.5", + "twd-js": "^0.8.0", "zustand": "^5.0.3" }, "devDependencies": { @@ -54,7 +58,6 @@ "npm-run-all": "^4.1.5", "nyc": "^17.1.0", "puppeteer": "^24.29.1", - "twd-js": "^0.8.0", "typescript": "~5.7.2", "typescript-eslint": "^8.26.1", "vite": "^6.3.1", diff --git a/scripts/run-tests-ci.js b/scripts/run-tests-ci.js index 6497393..5ec887f 100644 --- a/scripts/run-tests-ci.js +++ b/scripts/run-tests-ci.js @@ -1,12 +1,15 @@ +import fs from 'fs'; +import path from 'path'; import puppeteer from "puppeteer"; import { reportResults } from 'twd-js/runner-ci'; +let __dirname = path.resolve(); + const browser = await puppeteer.launch({ headless: true, - args: ['--lang=es-ES,es'], + args: ['--no-sandbox', '--disable-setuid-sandbox'], }); const page = await browser.newPage(); -await page.emulateTimezone('Europe/Madrid'); console.time('Total Test Time'); try { // Navigate to your development server @@ -40,7 +43,23 @@ try { // Display results in console reportResults(handlers, testStatus); - const coverage = await page.evaluate(() => window.__coverage__) + const coverage = await page.evaluate(() => window.__coverage__); + if (coverage) { + console.log('Collecting code coverage data...'); + const coverageDir = path.resolve(__dirname, './coverage'); + const nycDir = path.resolve(__dirname, './.nyc_output'); + if (!fs.existsSync(nycDir)) { + fs.mkdirSync(nycDir); + } + if (!fs.existsSync(coverageDir)) { + fs.mkdirSync(coverageDir); + } + const coveragePath = path.join(nycDir, 'out.json'); + fs.writeFileSync(coveragePath, JSON.stringify(coverage)); + console.log(`Code coverage data written to ${coveragePath}`); + } else { + console.log('No code coverage data found.'); + } // Exit with appropriate code const hasFailures = testStatus.some(test => test.status === 'fail'); diff --git a/src/twd-tests/helloWorld.twd.test.ts b/src/twd-tests/helloWorld.twd.test.ts index fdbbea7..3b1bb54 100644 --- a/src/twd-tests/helloWorld.twd.test.ts +++ b/src/twd-tests/helloWorld.twd.test.ts @@ -18,8 +18,6 @@ describe("Hello World Page", () => { counterButton.should("have.text", "Count is 2"); await userEvent.click(counterButton.el); - console.log(counterButton.el.textContent); - counterButton.should("have.text", "Count is 3"); }); -}); +}); \ No newline at end of file diff --git a/src/twd-tests/todoList.twd.test.ts b/src/twd-tests/todoList.twd.test.ts index 567e7c1..da5a914 100644 --- a/src/twd-tests/todoList.twd.test.ts +++ b/src/twd-tests/todoList.twd.test.ts @@ -16,8 +16,6 @@ describe("Todo List Page", () => { }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); - const todoList = await twd.getAll("[data-testid='todo-item']"); - expect(todoList).to.have.length(2); const todo1Title = await twd.get("[data-testid='todo-title-1']"); todo1Title.should("have.text", "Learn TWD"); const todo2Title = await twd.get("[data-testid='todo-title-2']"); @@ -32,34 +30,6 @@ describe("Todo List Page", () => { todo2Date.should("have.text", "Date: 2024-12-25"); }); - it("should delete a todo", async () => { - await twd.mockRequest("deleteTodo", { - method: "DELETE", - url: "/api/todos/1", - response: null, - status: 200, - }); - await twd.mockRequest("getTodoList", { - method: "GET", - url: "/api/todos", - response: todoListMock, - status: 200, - }); - await twd.visit("/todos"); - const deleteButton = await twd.get("[data-testid='delete-todo-1']"); - await twd.mockRequest("getTodoList", { - method: "GET", - url: "/api/todos", - response: todoListMock.filter((todo) => todo.id !== "1"), - status: 200, - }); - await userEvent.click(deleteButton.el); - await twd.waitForRequest("deleteTodo"); - await twd.waitForRequest("getTodoList"); - const todoList = await twd.getAll("[data-testid='todo-item']"); - expect(todoList).to.have.length(1); - }); - it("should create a todo", async () => { await twd.mockRequest("createTodo", { method: "POST", @@ -75,6 +45,8 @@ describe("Todo List Page", () => { }); await twd.visit("/todos"); await twd.waitForRequest("getTodoList"); + const noTodosMessage = await twd.get("[data-testid='no-todos-message']"); + noTodosMessage.should("be.visible"); await twd.mockRequest("getTodoList", { method: "GET", url: "/api/todos", @@ -83,8 +55,6 @@ describe("Todo List Page", () => { ], status: 200, }); - const noTodosMessage = await twd.get("[data-testid='no-todos-message']"); - noTodosMessage.should("be.visible"); const title = await twd.get("input[name='title']"); await userEvent.type(title.el, "Test Todo"); const description = await twd.get("input[name='description']"); @@ -103,4 +73,32 @@ describe("Todo List Page", () => { const todoList = await twd.getAll("[data-testid='todo-item']"); expect(todoList).to.have.length(1); }); + + it("should delete a todo", async () => { + await twd.mockRequest("deleteTodo", { + method: "DELETE", + url: "/api/todos/1", + response: null, + status: 200, + }); + await twd.mockRequest("getTodoList", { + method: "GET", + url: "/api/todos", + response: todoListMock, + status: 200, + }); + await twd.visit("/todos"); + const deleteButton = await twd.get("[data-testid='delete-todo-1']"); + await twd.mockRequest("getTodoList", { + method: "GET", + url: "/api/todos", + response: todoListMock.filter((todo) => todo.id !== "1"), + status: 200, + }); + await userEvent.click(deleteButton.el); + await twd.waitForRequest("deleteTodo"); + await twd.waitForRequest("getTodoList"); + const todoList = await twd.getAll("[data-testid='todo-item']"); + expect(todoList).to.have.length(1); + }); }); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 3fca629..7000673 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import path from "path" import tailwindcss from "@tailwindcss/vite" import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +// add plugin for code coverage import istanbul from 'vite-plugin-istanbul'; // https://vite.dev/config/ @@ -10,6 +11,7 @@ export default defineConfig({ plugins: [ react(), tailwindcss(), + // configure istanbul plugin istanbul({ include: 'src/**/*', exclude: ['node_modules', 'tests/'], @@ -27,4 +29,4 @@ export default defineConfig({ ignored: ["**/data/data.json", "**data/routes.json"], }, }, -}) +}) \ No newline at end of file