Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/actions/check-licenses/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: 🗳️ Check Licenses
description: 'Check package licenses for compliance'

runs:
using: 'composite'
steps:
- name: 🗳️ Check licenses
run: pnpm check-licenses
shell: bash
12 changes: 0 additions & 12 deletions .github/actions/code-analysis/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,3 @@ runs:
- name: 🫣 Lint
run: pnpm lint
shell: bash

- name: 🗳️ Check licenses
run: pnpm check-licenses
shell: bash

- name: 🧪 Test
run: pnpm test:coverage
shell: bash

- name: 📊 Aggregate coverage results
run: node scripts/aggregate-coverage-results.js
shell: bash
32 changes: 11 additions & 21 deletions .github/actions/node-setup/action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: 💻 Node.js setup
description: 'Setup Node.js environment and install dependencies'

inputs:
install-dependencies:
description: 'Set to false to skip pnpm install (for tooling-only jobs)'
default: 'true'

runs:
using: 'composite'
steps:
Expand All @@ -9,37 +14,22 @@ runs:
shell: bash

- name: 🎰 Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version-file: 'package.json'

- name: 🎈 Install pNPM
uses: pnpm/action-setup@v3
with:
run_install: false

- name: 📀 Get pnpm store directory
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
shell: bash

- name: 💾 Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
cache: 'pnpm'
cache-dependency-path: 'pnpm-lock.yaml'

- name: 📥 Install dependencies
if: inputs.install-dependencies == 'true'
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
shell: bash

- name: 💾 Cache turbo build setup
uses: actions/cache@v4
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
key: ${{ runner.os }}-turbo-${{ github.head_ref || github.ref_name }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-turbo-${{ github.head_ref || github.ref_name }}-
${{ runner.os }}-turbo-
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: 🧪 CI

on:
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:

permissions:
contents: read # required so checkout/actions using GITHUB_TOKEN can read repo

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-24.04
steps:
- name: 📥 Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: 💻 Node setup
uses: ./.github/actions/node-setup

- name: 👁️‍🗨️ Code Analysis
uses: ./.github/actions/code-analysis

licenses:
runs-on: ubuntu-24.04
steps:
- name: 📥 Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: 💻 Node setup
uses: ./.github/actions/node-setup

- name: 🗳️ Check Licenses
uses: ./.github/actions/check-licenses

build:
runs-on: ubuntu-24.04
steps:
- name: 📥 Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: 💻 Node setup
uses: ./.github/actions/node-setup

- name: 🏗️ Build packages
run: pnpm build
shell: bash
25 changes: 0 additions & 25 deletions .github/workflows/code-analysis.yml

This file was deleted.

126 changes: 126 additions & 0 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: 📊 Tests & Coverage

# Configuration
# Set to 'true' for monorepo, 'false' for single repository
env:
MONOREPO: 'true'
BASE_BRANCH: 'main'

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened]

concurrency:
group: coverage-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read # required so checkout/actions using GITHUB_TOKEN can read repo
actions: read # required to download base coverage artifact via workflow run APIs
pull-requests: write # required to post/update coverage diff comments on PRs

jobs:
coverage:
runs-on: ubuntu-24.04
permissions:
actions: read # download base coverage artifact via workflow run APIs
contents: read # checkout
pull-requests: write # comment coverage diff on PRs
env:
EVENT_TYPE: ${{ github.event_name == 'push' && 'push' || 'pr' }}
# Workflow-level env variables (MONOREPO, BASE_BRANCH) are automatically inherited by jobs
steps:
- name: 📥 Checkout Repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5

- name: 💻 Node setup
uses: ./.github/actions/node-setup

- name: 🧪 Test
run: pnpm test:coverage
shell: bash
# Note: If tests fail, the workflow will fail and coverage steps won't run.
# This ensures we only report coverage for passing tests.

- name: 📊 Aggregate coverage results and create summary
# MONOREPO is inherited from workflow-level env
run: node scripts/aggregate-coverage-results.js
shell: bash

- name: 🔍 Locate base coverage workflow run
id: locate-base-coverage
if: env.EVENT_TYPE == 'pr'
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
env:
BASE_WORKFLOW: coverage.yml
# BASE_BRANCH is inherited from workflow-level env
with:
script: |
const workflowId = process.env.BASE_WORKFLOW;
const baseBranch = process.env.BASE_BRANCH || 'main';

// Get the latest successful workflow run from the base branch
const { data } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: workflowId,
branch: baseBranch,
per_page: 20,
});

const run = data.workflow_runs.find((run) => run.conclusion === 'success');
if (run) {
core.info(`Found base coverage run ${run.id} from ${baseBranch} branch`);
core.setOutput('run-id', String(run.id));
} else {
core.warning(`No successful base coverage run found for ${baseBranch} branch`);
core.setOutput('run-id', '');
}

- name: 📥 Download base branch coverage artifact
if: env.EVENT_TYPE == 'pr' && steps.locate-base-coverage.outputs.run-id != ''
continue-on-error: true
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: base-coverage
path: base-coverage
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ steps.locate-base-coverage.outputs.run-id }}

- name: 💬 Comment coverage diff
if: env.EVENT_TYPE == 'pr'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_COVERAGE_ROOT: base-coverage
# MONOREPO and BASE_BRANCH are inherited from workflow-level env
run: node scripts/comment-coverage-diff.js
shell: bash

- name: ♻️ Collect coverage summaries
if: env.EVENT_TYPE == 'push' && github.ref_name == 'main'
# MONOREPO is inherited from workflow-level env
run: |
rm -rf base-coverage
if [ "$MONOREPO" = "true" ]; then
rsync -a \
--include '*/' \
--include 'coverage-summary.json' \
--exclude '*' \
coverage/ base-coverage/
else
mkdir -p base-coverage
cp coverage/coverage-summary.json base-coverage/coverage-summary.json
fi
shell: bash

- name: 📤 Upload coverage artifact
if: env.EVENT_TYPE == 'push' && github.ref_name == 'main'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: base-coverage
path: base-coverage
retention-days: 14 # Artifacts expire after 14 days. Adjust if needed, but note that PRs opened after expiration won't have base coverage to compare against.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ next-env.d.ts

# Testing
/coverage
/base-coverage
.actrc

# Cache
.turbo
Expand Down
28 changes: 28 additions & 0 deletions apps/frontend/src/lib/i18n/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { safeImportNamespace } from '../utils';

// Mock dynamic imports
jest.mock('../locales/en/common.json', () => ({ default: { hello: 'Hello' } }), { virtual: true });
jest.mock('../locales/hr/common.json', () => ({ default: { hello: 'Zdravo' } }), { virtual: true });

describe('safeImportNamespace', () => {
it('successfully imports existing namespace', async () => {
// Note: This test may need adjustment based on how Jest handles dynamic imports
// The actual implementation uses dynamic imports which can be tricky to test
const result = await safeImportNamespace('en', 'common');
expect(result).toBeDefined();
});

it('throws error for missing namespace', async () => {
await expect(safeImportNamespace('en', 'nonexistent')).rejects.toThrow('Missing translation namespace');
});

it('throws error for missing locale', async () => {
await expect(safeImportNamespace('nonexistent', 'common')).rejects.toThrow('Missing translation namespace');
});

it('rethrows non-MODULE_NOT_FOUND errors', async () => {
// This would require mocking the import to throw a different error
// For now, we'll test the error handling logic exists
await expect(safeImportNamespace('invalid', 'common')).rejects.toThrow();
});
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"test": "turbo run test --parallel --log-order=grouped --continue",
"test:watch": "turbo run test:watch --ui=tui --continue",
"test:coverage": "turbo run test:coverage --parallel --log-order=grouped --continue",
"test:coverage:aggregate": "pnpm test:coverage && MONOREPO=true node scripts/aggregate-coverage-results.js",
"test:coverage:watch": "turbo run test:coverage:watch --ui=tui --continue"
},
"devDependencies": {
Expand Down
40 changes: 40 additions & 0 deletions packages/ui/src/components/__tests__/card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react';

import { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '../card';
import { Button } from '../button';

describe('Card component suite', () => {
it('renders all structural slots with custom classNames', () => {
render(
<Card className="test-card" data-testid="card-root">
<CardHeader className="header-slot">
<CardTitle>Plan</CardTitle>
<CardDescription>Choose the perfect plan</CardDescription>
<CardAction>
<Button>Primary action</Button>
</CardAction>
</CardHeader>
<CardContent className="content-slot">Card body content</CardContent>
<CardFooter className="footer-slot">Footer CTA</CardFooter>
</Card>
);

expect(screen.getByTestId('card-root')).toHaveClass('test-card');
expect(screen.getByText('Plan')).toHaveAttribute('data-slot', 'card-title');
expect(screen.getByText('Choose the perfect plan')).toHaveAttribute('data-slot', 'card-description');
expect(screen.getByRole('button', { name: /primary action/i })).toBeInTheDocument();
expect(screen.getByText('Card body content')).toHaveClass('content-slot');
expect(screen.getByText('Footer CTA')).toHaveClass('footer-slot');
});

it('forwards arbitrary props down to the DOM nodes', () => {
render(
<Card id="pricing-card" aria-label="Pricing overview">
<CardContent>Details</CardContent>
</Card>
);

const card = screen.getByLabelText('Pricing overview');
expect(card).toHaveAttribute('id', 'pricing-card');
});
});
Loading
Loading