diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1afae1..3d51b49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,12 +1,18 @@ name: CI + on: push: branches: + - core - basic pull_request: branches: + - core - basic +env: + NODE_VERSION: "22.x" + jobs: lint-tests: uses: ./.github/workflows/run-tests.yml @@ -15,13 +21,69 @@ jobs: test-command: pnpm lint unit-tests: - uses: ./.github/workflows/run-tests.yml - with: - test-type: unit - test-command: pnpm test:ci + runs-on: ubuntu-latest + + # For every direct push to target branches or when it's active PR + if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Enable corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + - name: Setup pnpm cache + uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile --ignore-scripts + + - name: Run coverage + run: pnpm test:coverage + + - name: Run unit tests + run: pnpm test:ci + + - name: Upload test results + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: unit-test-results + path: reports/**/* + + - name: Upload coverage report + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: unit-coverage-report + path: coverage/**/* storybook-tests: runs-on: ubuntu-latest + env: + STORYBOOK_DISABLE_TELEMETRY: 1 + STORYBOOK: "true" + CI: true + + # For every direct push to target branches or when it's active PR + if: ${{ github.event_name == 'push' || !github.event.pull_request.draft }} steps: - name: Checkout uses: actions/checkout@v4 @@ -29,12 +91,11 @@ jobs: - name: Enable corepack run: corepack enable - # https://github.com/storybookjs/test-runner/issues/301 - # - name: Install Node.js - # uses: actions/setup-node@v3 - # with: - # node-version: "16.x" - # cache: "pnpm" + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" - name: Get pnpm store directory id: pnpm-cache @@ -53,8 +114,8 @@ jobs: - name: Install dependencies run: pnpm install --no-frozen-lockfile --ignore-scripts - - name: Install Playwright - run: pnpm exec playwright install --with-deps + - name: Install Playwright browsers + run: pnpm exec playwright install chromium --with-deps --only-shell - name: Build Storybook run: pnpm build-storybook --quiet @@ -63,13 +124,21 @@ jobs: run: | pnpm exec concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \ "pnpm exec http-server storybook-static --port 6006 --silent" \ - "pnpm exec wait-on tcp:127.0.0.1:6006 && pnpm exec test-storybook --json --outputFile storybook-report.json a" + "pnpm exec wait-on tcp:6006 && pnpm exec test-storybook --coverage --coverageDirectory=coverage --excludeTags=\"!test\" --junit --outputFile reports/storybook-report.json" - - name: Upload json report artifacts + - name: Upload test results + if: success() || failure() uses: actions/upload-artifact@v4 with: - name: storybook-report - path: storybook-report.json + name: storybook-test-results + path: reports/**/* + + - name: Upload coverage report + if: success() || failure() + uses: actions/upload-artifact@v4 + with: + name: storybook-coverage-report + path: coverage/**/* build: needs: [lint-tests, unit-tests, storybook-tests] @@ -117,8 +186,49 @@ jobs: name: build path: dist + test-reports: + needs: [unit-tests, storybook-tests] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download Artifacts + uses: actions/download-artifact@v4 + + - name: Generate Full Coverage Report + uses: danielpalme/ReportGenerator-GitHub-Action@v5 + with: + reports: "unit-coverage-report/lcov.info;storybook-coverage-report/lcov.info" + reporttypes: "MarkdownSummaryGithub;JsonSummary;Html" + targetdir: coverage-report + + - name: Add Coverage Report Comment to PR + if: github.event_name == 'pull_request' + run: gh pr comment $PR_NUMBER --body-file coverage-report/SummaryGithub.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + + - name: Publish Coverage Report in Build Summary + run: cat coverage-report/SummaryGithub.md >> $GITHUB_STEP_SUMMARY + + - name: Upload Full Coverage Report + uses: actions/upload-artifact@v4 + with: + name: full-coverage-report + path: coverage-report/**/* + + - name: Publish Test Results + uses: dorny/test-reporter@v2 + id: test-reporter + with: + name: Test Results + path: unit-test-results/**/*.xml,storybook-test-results/**/*.xml + reporter: jest-junit + fail-on-error: false + deploy: - needs: build + needs: [build, test-reports] runs-on: ubuntu-latest steps: - name: Get build artifacts diff --git a/.storybook/main.ts b/.storybook/main.ts index df2d266..8c309db 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,3 +1,4 @@ +import type { AddonOptionsVite } from "@storybook/addon-coverage"; import type { StorybookConfig } from "@storybook/react-vite"; import * as tsconfigPaths from "vite-tsconfig-paths"; @@ -7,6 +8,15 @@ const config: StorybookConfig = { addons: [ "@storybook/addon-links", "@storybook/addon-a11y", + { + name: "@storybook/addon-coverage", + options: { + istanbul: { + include: ["src/**/*.tsx"], + exclude: ["src/**/*.ts", "src/test-lib/*.tsx"], + }, + } satisfies AddonOptionsVite, + }, "@storybook/addon-docs", ], @@ -28,6 +38,12 @@ const config: StorybookConfig = { typescript: { reactDocgen: "react-docgen-typescript", }, + + build: { + test: { + disabledAddons: ["@storybook/addon-docs"], + }, + }, }; export default config; diff --git a/README.md b/README.md index 77c5c4d..4b7cf96 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This version is free of any libraries. If the `core` version doesn't match your - TypeScript support. - [Devcontainer](https://code.visualstudio.com/docs/devcontainers/containers) config for VS Code. - [PNPM](https://pnpm.io/) as a package manager. -- CI setup (automate tests, build, deploy draft) with [GitHub Actions](https://docs.github.com/en/actions). +- CI setup (tests, build, tests coverage report, deploy draft) with [GitHub Actions](https://docs.github.com/en/actions). - [Github Copilot](https://github.com/features/copilot) instructions configured for efficient chat and agent usage. ## Extended version - `core` @@ -91,14 +91,17 @@ Learn more about using this template in practice below. ## Basic commands -| Command | Description | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `pnpm dev` | Runs dev server with the HMR locally on port `5173` | -| `pnpm build` | Builds optimized app package | -| `pnpm test` | Runs unit tests | -| `pnpm storybook` | Runs a Storybook locally on port `6006` | -| `pnpm test-storybook` | Runs integration tests (requires a running Storybook on port `6006` - more info [here](https://storybook.js.org/blog/interaction-testing-with-storybook/)) | -| `pnpm build-storybook` | Builds static app with [a Storybook's content](https://storybook.js.org/docs/react/sharing/publish-storybook) | +| Command | Description | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `pnpm dev` | Runs dev server with the HMR locally on port `5173` | +| `pnpm lint` | Checks for lint errors | +| `pnpm build` | Builds optimized app package | +| `pnpm test` | Runs unit tests | +| `pnpm test:coverage` | Runs unit tests with coverage | +| `pnpm storybook` | Runs a Storybook locally on port `6006` | +| `pnpm test-storybook` | Runs integration tests (requires a running Storybook on port `6006` - more info [here](https://storybook.js.org/blog/interaction-testing-with-storybook/)) | +| `pnpm test-storybook:coverage` | Runs integration tests with coverage | +| `pnpm build-storybook` | Builds static app with [a Storybook's content](https://storybook.js.org/docs/react/sharing/publish-storybook) | # Contributing diff --git a/package.json b/package.json index ad837c3..96cf9e9 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "dev": "vite", "build": "tsc && vite build", "test": "vitest", - "test:ci": "vitest run --reporter verbose --reporter=junit --outputFile=./unit-report.xml", + "test:ci": "vitest run --reporter verbose --reporter=junit --outputFile=./reports/unit-report.xml --coverage --coverage.reporter=lcov --coverage.reportsDirectory=./coverage", + "test:coverage": "vitest run --coverage", "preview": "vite preview", "lint": "CI=true eslint . --ext ts,tsx --ignore-pattern '*.d.ts' --report-unused-disable-directives --max-warnings 0", "prepare": "husky install", "storybook": "storybook dev -p 6006", "test-storybook": "test-storybook --watch", + "test-storybook:coverage": "test-storybook --coverage", "build-storybook": "storybook build" }, "dependencies": { @@ -23,6 +25,7 @@ "devDependencies": { "@eslint/js": "9.24.0", "@storybook/addon-a11y": "9.0.15", + "@storybook/addon-coverage": "2.0.0", "@storybook/addon-docs": "9.0.15", "@storybook/addon-links": "9.0.15", "@storybook/react-vite": "9.0.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf6967d..a0bc673 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: "@storybook/addon-a11y": specifier: 9.0.15 version: 9.0.15(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) + "@storybook/addon-coverage": + specifier: ^2.0.0 + version: 2.0.0(vite@7.0.1(@types/node@20.14.11)) "@storybook/addon-docs": specifier: 9.0.15 version: 9.0.15(@types/react@19.1.8)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3)) @@ -102,7 +105,7 @@ importers: specifier: 8.0.3 version: 8.0.3 jsdom: - specifier: ^26.1.0 + specifier: 26.1.0 version: 26.1.0 playwright: specifier: 1.52.0 @@ -2127,6 +2130,12 @@ packages: integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==, } + "@jsdevtools/coverage-istanbul-loader@3.0.5": + resolution: + { + integrity: sha512-EUCPEkaRPvmHjWAAZkWMT7JDzpw7FKB00WTISaiXsbNOd5hCHg77XLA8sLYLFDo1zepYLo2w7GstN8YBqRXZfA==, + } + "@mdx-js/react@3.0.1": resolution: { @@ -2417,6 +2426,12 @@ packages: peerDependencies: storybook: ^9.0.15 + "@storybook/addon-coverage@2.0.0": + resolution: + { + integrity: sha512-KRqpV7+kjrKos/Im7UKqPC5MqDsZvsNl1N+Ie3TAZQd9nR/Z/4rC0oOlqW1FCRYDeyT44IQes/Km42sP8ow36g==, + } + "@storybook/addon-docs@9.0.15": resolution: { @@ -3297,6 +3312,14 @@ packages: } engines: { node: ">=8" } + ajv-keywords@3.5.2: + resolution: + { + integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==, + } + peerDependencies: + ajv: ^6.9.1 + ajv@6.12.6: resolution: { @@ -3651,6 +3674,12 @@ packages: } engines: { node: ">=12.0.0" } + big.js@5.2.2: + resolution: + { + integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==, + } + binary-extensions@2.3.0: resolution: { @@ -4309,6 +4338,13 @@ packages: integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==, } + emojis-list@3.0.0: + resolution: + { + integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==, + } + engines: { node: ">= 4" } + entities@1.1.2: resolution: { @@ -4718,6 +4754,13 @@ packages: } engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + espree@9.6.1: + resolution: + { + integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==, + } + engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + esprima@4.0.1: resolution: { @@ -6187,6 +6230,13 @@ packages: integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, } + loader-utils@2.0.4: + resolution: + { + integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==, + } + engines: { node: ">=8.9.0" } + locate-path@5.0.0: resolution: { @@ -6315,6 +6365,12 @@ packages: } engines: { node: ">= 0.4" } + merge-source-map@1.1.0: + resolution: + { + integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==, + } + merge-stream@2.0.0: resolution: { @@ -7260,6 +7316,13 @@ packages: integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==, } + schema-utils@2.7.1: + resolution: + { + integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==, + } + engines: { node: ">= 8.9.0" } + secure-compare@3.0.1: resolution: { @@ -7415,6 +7478,13 @@ packages: } engines: { node: ">=0.10.0" } + source-map@0.7.4: + resolution: + { + integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==, + } + engines: { node: ">= 8" } + spawn-command@0.0.2: resolution: { @@ -8094,6 +8164,14 @@ packages: vue-tsc: optional: true + vite-plugin-istanbul@6.0.2: + resolution: + { + integrity: sha512-0/sKwjEEIwbEyl43xX7onX3dIbMJAsigNsKyyVPalG1oRFo5jn3qkJbS2PUfp9wrr3piy1eT6qRoeeum2p4B2A==, + } + peerDependencies: + vite: ">=4 <=6" + vite-tsconfig-paths@5.1.4: resolution: { @@ -9915,6 +9993,16 @@ snapshots: "@jridgewell/resolve-uri": 3.1.2 "@jridgewell/sourcemap-codec": 1.5.4 + "@jsdevtools/coverage-istanbul-loader@3.0.5": + dependencies: + convert-source-map: 1.9.0 + istanbul-lib-instrument: 4.0.3 + loader-utils: 2.0.4 + merge-source-map: 1.1.0 + schema-utils: 2.7.1 + transitivePeerDependencies: + - supports-color + "@mdx-js/react@3.0.1(@types/react@19.1.8)(react@19.1.0)": dependencies: "@types/mdx": 2.0.13 @@ -10047,6 +10135,20 @@ snapshots: axe-core: 4.9.1 storybook: 9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3) + "@storybook/addon-coverage@2.0.0(vite@7.0.1(@types/node@20.14.11))": + dependencies: + "@istanbuljs/load-nyc-config": 1.1.0 + "@jsdevtools/coverage-istanbul-loader": 3.0.5 + "@types/istanbul-lib-coverage": 2.0.6 + convert-source-map: 2.0.0 + espree: 9.6.1 + istanbul-lib-instrument: 6.0.3 + test-exclude: 6.0.0 + vite-plugin-istanbul: 6.0.2(vite@7.0.1(@types/node@20.14.11)) + transitivePeerDependencies: + - supports-color + - vite + "@storybook/addon-docs@9.0.15(@types/react@19.1.8)(storybook@9.0.15(@testing-library/dom@10.4.0)(prettier@3.5.3))": dependencies: "@mdx-js/react": 3.0.1(@types/react@19.1.8)(react@19.1.0) @@ -10680,6 +10782,10 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -10964,6 +11070,8 @@ snapshots: dependencies: open: 8.4.2 + big.js@5.2.2: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -11343,6 +11451,8 @@ snapshots: emoji-regex@9.2.2: {} + emojis-list@3.0.0: {} + entities@1.1.2: {} entities@2.2.0: {} @@ -11843,6 +11953,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 + espree@9.6.1: + dependencies: + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} esquery@1.6.0: @@ -12953,6 +13069,12 @@ snapshots: lines-and-columns@1.2.4: {} + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -13015,6 +13137,10 @@ snapshots: math-intrinsics@1.1.0: {} + merge-source-map@1.1.0: + dependencies: + source-map: 0.6.1 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -13586,6 +13712,12 @@ snapshots: scheduler@0.26.0: {} + schema-utils@2.7.1: + dependencies: + "@types/json-schema": 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + secure-compare@3.0.1: {} semver@6.3.1: {} @@ -13673,6 +13805,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.4: {} + spawn-command@0.0.2: {} spawn-wrap@2.0.0: @@ -14121,6 +14255,18 @@ snapshots: optionator: 0.9.4 typescript: 5.8.2 + vite-plugin-istanbul@6.0.2(vite@7.0.1(@types/node@20.14.11)): + dependencies: + "@istanbuljs/load-nyc-config": 1.1.0 + espree: 10.3.0 + istanbul-lib-instrument: 6.0.3 + picocolors: 1.1.1 + source-map: 0.7.4 + test-exclude: 6.0.0 + vite: 7.0.1(@types/node@20.14.11) + transitivePeerDependencies: + - supports-color + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@7.0.1(@types/node@20.14.11)): dependencies: debug: 4.4.1 diff --git a/vitest.config.ts b/vitest.config.ts index 147e070..d7a7c8e 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -25,7 +25,7 @@ export default defineConfig((env) => "**/*{.,-}stories.?(c|m)[jt]s?(x)", ...coverageConfigDefaults.exclude, ], - reportsDirectory: "./reports", + reportsDirectory: "./coverage", }, }, })