mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-21 18:58:04 +00:00
feat(ui): setup vitest with react testing library and TDD workflow (#9925)
This commit is contained in:
50
.github/workflows/ui-tests.yml
vendored
50
.github/workflows/ui-tests.yml
vendored
@@ -44,6 +44,35 @@ jobs:
|
|||||||
ui/README.md
|
ui/README.md
|
||||||
ui/AGENTS.md
|
ui/AGENTS.md
|
||||||
|
|
||||||
|
- name: Get changed source files for targeted tests
|
||||||
|
id: changed-source
|
||||||
|
if: steps.check-changes.outputs.any_changed == 'true'
|
||||||
|
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
ui/**/*.ts
|
||||||
|
ui/**/*.tsx
|
||||||
|
files_ignore: |
|
||||||
|
ui/**/*.test.ts
|
||||||
|
ui/**/*.test.tsx
|
||||||
|
ui/**/*.spec.ts
|
||||||
|
ui/**/*.spec.tsx
|
||||||
|
ui/vitest.config.ts
|
||||||
|
ui/vitest.setup.ts
|
||||||
|
|
||||||
|
- name: Check for critical path changes (run all tests)
|
||||||
|
id: critical-changes
|
||||||
|
if: steps.check-changes.outputs.any_changed == 'true'
|
||||||
|
uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
ui/lib/**
|
||||||
|
ui/types/**
|
||||||
|
ui/config/**
|
||||||
|
ui/middleware.ts
|
||||||
|
ui/vitest.config.ts
|
||||||
|
ui/vitest.setup.ts
|
||||||
|
|
||||||
- name: Setup Node.js ${{ env.NODE_VERSION }}
|
- name: Setup Node.js ${{ env.NODE_VERSION }}
|
||||||
if: steps.check-changes.outputs.any_changed == 'true'
|
if: steps.check-changes.outputs.any_changed == 'true'
|
||||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||||
@@ -83,6 +112,27 @@ jobs:
|
|||||||
if: steps.check-changes.outputs.any_changed == 'true'
|
if: steps.check-changes.outputs.any_changed == 'true'
|
||||||
run: pnpm run healthcheck
|
run: pnpm run healthcheck
|
||||||
|
|
||||||
|
- name: Run unit tests (all - critical paths changed)
|
||||||
|
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed == 'true'
|
||||||
|
run: |
|
||||||
|
echo "Critical paths changed - running ALL unit tests"
|
||||||
|
pnpm run test:run
|
||||||
|
|
||||||
|
- name: Run unit tests (related to changes only)
|
||||||
|
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files != ''
|
||||||
|
run: |
|
||||||
|
echo "Running tests related to changed files:"
|
||||||
|
echo "${{ steps.changed-source.outputs.all_changed_files }}"
|
||||||
|
# Convert space-separated to vitest related format (remove ui/ prefix for relative paths)
|
||||||
|
CHANGED_FILES=$(echo "${{ steps.changed-source.outputs.all_changed_files }}" | tr ' ' '\n' | sed 's|^ui/||' | tr '\n' ' ')
|
||||||
|
pnpm exec vitest related $CHANGED_FILES --run
|
||||||
|
|
||||||
|
- name: Run unit tests (test files only changed)
|
||||||
|
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed != 'true' && steps.changed-source.outputs.all_changed_files == ''
|
||||||
|
run: |
|
||||||
|
echo "Only test files changed - running ALL unit tests"
|
||||||
|
pnpm run test:run
|
||||||
|
|
||||||
- name: Build application
|
- name: Build application
|
||||||
if: steps.check-changes.outputs.any_changed == 'true'
|
if: steps.check-changes.outputs.any_changed == 'true'
|
||||||
run: pnpm run build
|
run: pnpm run build
|
||||||
|
|||||||
15
AGENTS.md
15
AGENTS.md
@@ -24,6 +24,8 @@ Use these skills for detailed patterns on-demand:
|
|||||||
| `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) |
|
| `zod-4` | New API (z.email(), z.uuid()) | [SKILL.md](skills/zod-4/SKILL.md) |
|
||||||
| `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) |
|
| `zustand-5` | Persist, selectors, slices | [SKILL.md](skills/zustand-5/SKILL.md) |
|
||||||
| `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) |
|
| `ai-sdk-5` | UIMessage, streaming, LangChain | [SKILL.md](skills/ai-sdk-5/SKILL.md) |
|
||||||
|
| `vitest` | Unit testing, React Testing Library | [SKILL.md](skills/vitest/SKILL.md) |
|
||||||
|
| `tdd` | Test-Driven Development workflow | [SKILL.md](skills/tdd/SKILL.md) |
|
||||||
|
|
||||||
### Prowler-Specific Skills
|
### Prowler-Specific Skills
|
||||||
| Skill | Description | URL |
|
| Skill | Description | URL |
|
||||||
@@ -56,8 +58,8 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
|||||||
| Add changelog entry for a PR or feature | `prowler-changelog` |
|
| Add changelog entry for a PR or feature | `prowler-changelog` |
|
||||||
| Adding DRF pagination or permissions | `django-drf` |
|
| Adding DRF pagination or permissions | `django-drf` |
|
||||||
| Adding new providers | `prowler-provider` |
|
| Adding new providers | `prowler-provider` |
|
||||||
| Adding services to existing providers | `prowler-provider` |
|
|
||||||
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
|
| Adding privilege escalation detection queries | `prowler-attack-paths-query` |
|
||||||
|
| Adding services to existing providers | `prowler-provider` |
|
||||||
| After creating/modifying a skill | `skill-sync` |
|
| After creating/modifying a skill | `skill-sync` |
|
||||||
| App Router / Server Actions | `nextjs-15` |
|
| App Router / Server Actions | `nextjs-15` |
|
||||||
| Building AI chat features | `ai-sdk-5` |
|
| Building AI chat features | `ai-sdk-5` |
|
||||||
@@ -76,30 +78,38 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
|||||||
| Creating/updating compliance frameworks | `prowler-compliance` |
|
| Creating/updating compliance frameworks | `prowler-compliance` |
|
||||||
| Debug why a GitHub Actions job is failing | `prowler-ci` |
|
| Debug why a GitHub Actions job is failing | `prowler-ci` |
|
||||||
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
|
| Fill .github/pull_request_template.md (Context/Description/Steps to review/Checklist) | `prowler-pr` |
|
||||||
|
| Fixing bug | `tdd` |
|
||||||
| General Prowler development questions | `prowler` |
|
| General Prowler development questions | `prowler` |
|
||||||
| Implementing JSON:API endpoints | `django-drf` |
|
| Implementing JSON:API endpoints | `django-drf` |
|
||||||
|
| Implementing feature | `tdd` |
|
||||||
| Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` |
|
| Inspect PR CI checks and gates (.github/workflows/*) | `prowler-ci` |
|
||||||
| Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` |
|
| Inspect PR CI workflows (.github/workflows/*): conventional-commit, pr-check-changelog, pr-conflict-checker, labeler | `prowler-pr` |
|
||||||
| Mapping checks to compliance controls | `prowler-compliance` |
|
| Mapping checks to compliance controls | `prowler-compliance` |
|
||||||
| Mocking AWS with moto in tests | `prowler-test-sdk` |
|
| Mocking AWS with moto in tests | `prowler-test-sdk` |
|
||||||
| Modifying API responses | `jsonapi` |
|
| Modifying API responses | `jsonapi` |
|
||||||
|
| Modifying component | `tdd` |
|
||||||
|
| Refactoring code | `tdd` |
|
||||||
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
|
| Regenerate AGENTS.md Auto-invoke tables (sync.sh) | `skill-sync` |
|
||||||
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
|
| Review PR requirements: template, title conventions, changelog gate | `prowler-pr` |
|
||||||
| Review changelog format and conventions | `prowler-changelog` |
|
| Review changelog format and conventions | `prowler-changelog` |
|
||||||
| Reviewing JSON:API compliance | `jsonapi` |
|
| Reviewing JSON:API compliance | `jsonapi` |
|
||||||
| Reviewing compliance framework PRs | `prowler-compliance-review` |
|
| Reviewing compliance framework PRs | `prowler-compliance-review` |
|
||||||
| Testing RLS tenant isolation | `prowler-test-api` |
|
| Testing RLS tenant isolation | `prowler-test-api` |
|
||||||
|
| Testing hooks or utilities | `vitest` |
|
||||||
| Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` |
|
| Troubleshoot why a skill is missing from AGENTS.md auto-invoke | `skill-sync` |
|
||||||
| Understand CODEOWNERS/labeler-based automation | `prowler-ci` |
|
| Understand CODEOWNERS/labeler-based automation | `prowler-ci` |
|
||||||
| Understand PR title conventional-commit validation | `prowler-ci` |
|
| Understand PR title conventional-commit validation | `prowler-ci` |
|
||||||
| Understand changelog gate and no-changelog label behavior | `prowler-ci` |
|
| Understand changelog gate and no-changelog label behavior | `prowler-ci` |
|
||||||
| Understand review ownership with CODEOWNERS | `prowler-pr` |
|
| Understand review ownership with CODEOWNERS | `prowler-pr` |
|
||||||
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
||||||
|
| Updating README.md provider statistics table | `prowler-readme-table` |
|
||||||
|
| Updating checks, services, compliance, or categories count in README.md | `prowler-readme-table` |
|
||||||
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
|
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
|
||||||
| Updating existing checks and metadata | `prowler-sdk-check` |
|
| Updating existing checks and metadata | `prowler-sdk-check` |
|
||||||
| Using Zustand stores | `zustand-5` |
|
| Using Zustand stores | `zustand-5` |
|
||||||
| Working on MCP server tools | `prowler-mcp` |
|
| Working on MCP server tools | `prowler-mcp` |
|
||||||
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
||||||
|
| Working on task | `tdd` |
|
||||||
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
||||||
| Working with Tailwind classes | `tailwind-4` |
|
| Working with Tailwind classes | `tailwind-4` |
|
||||||
| Writing Playwright E2E tests | `playwright` |
|
| Writing Playwright E2E tests | `playwright` |
|
||||||
@@ -107,9 +117,12 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
|||||||
| Writing Prowler SDK tests | `prowler-test-sdk` |
|
| Writing Prowler SDK tests | `prowler-test-sdk` |
|
||||||
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
||||||
| Writing Python tests with pytest | `pytest` |
|
| Writing Python tests with pytest | `pytest` |
|
||||||
|
| Writing React component tests | `vitest` |
|
||||||
| Writing React components | `react-19` |
|
| Writing React components | `react-19` |
|
||||||
| Writing TypeScript types/interfaces | `typescript` |
|
| Writing TypeScript types/interfaces | `typescript` |
|
||||||
|
| Writing Vitest tests | `vitest` |
|
||||||
| Writing documentation | `prowler-docs` |
|
| Writing documentation | `prowler-docs` |
|
||||||
|
| Writing unit tests for UI | `vitest` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,18 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
|||||||
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
|
| Creating ViewSets, serializers, or filters in api/ | `django-drf` |
|
||||||
| Creating a git commit | `prowler-commit` |
|
| Creating a git commit | `prowler-commit` |
|
||||||
| Creating/modifying models, views, serializers | `prowler-api` |
|
| Creating/modifying models, views, serializers | `prowler-api` |
|
||||||
|
| Fixing bug | `tdd` |
|
||||||
| Implementing JSON:API endpoints | `django-drf` |
|
| Implementing JSON:API endpoints | `django-drf` |
|
||||||
|
| Implementing feature | `tdd` |
|
||||||
| Modifying API responses | `jsonapi` |
|
| Modifying API responses | `jsonapi` |
|
||||||
|
| Modifying component | `tdd` |
|
||||||
|
| Refactoring code | `tdd` |
|
||||||
| Review changelog format and conventions | `prowler-changelog` |
|
| Review changelog format and conventions | `prowler-changelog` |
|
||||||
| Reviewing JSON:API compliance | `jsonapi` |
|
| Reviewing JSON:API compliance | `jsonapi` |
|
||||||
| Testing RLS tenant isolation | `prowler-test-api` |
|
| Testing RLS tenant isolation | `prowler-test-api` |
|
||||||
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
||||||
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
|
| Updating existing Attack Paths queries | `prowler-attack-paths-query` |
|
||||||
|
| Working on task | `tdd` |
|
||||||
| Writing Prowler API tests | `prowler-test-api` |
|
| Writing Prowler API tests | `prowler-test-api` |
|
||||||
| Writing Python tests with pytest | `pytest` |
|
| Writing Python tests with pytest | `pytest` |
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
|||||||
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
|
- Attack Paths: Mark attack Paths scan as failed when Celery task fails outside job error handling [(#10065)](https://github.com/prowler-cloud/prowler/pull/10065)
|
||||||
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
|
- Attack Paths: Remove legacy per-scan `graph_database` and `is_graph_database_deleted` fields from AttackPathsScan model [(#10077)](https://github.com/prowler-cloud/prowler/pull/10077)
|
||||||
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
|
- Attack Paths: Add `graph_data_ready` field to decouple query availability from scan state [(#10089)](https://github.com/prowler-cloud/prowler/pull/10089)
|
||||||
|
- AI agent guidelines with TDD and testing skills references [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
|
||||||
|
|
||||||
### 🐞 Fixed
|
### 🐞 Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ Reusable patterns for common technologies:
|
|||||||
| `nextjs-15` | App Router, Server Actions, streaming |
|
| `nextjs-15` | App Router, Server Actions, streaming |
|
||||||
| `tailwind-4` | cn() utility, Tailwind 4 patterns |
|
| `tailwind-4` | cn() utility, Tailwind 4 patterns |
|
||||||
| `playwright` | Page Object Model, selectors |
|
| `playwright` | Page Object Model, selectors |
|
||||||
|
| `vitest` | Unit testing, React Testing Library |
|
||||||
|
| `tdd` | Test-Driven Development workflow |
|
||||||
| `pytest` | Fixtures, mocking, markers |
|
| `pytest` | Fixtures, mocking, markers |
|
||||||
| `django-drf` | ViewSets, Serializers, Filters |
|
| `django-drf` | ViewSets, Serializers, Filters |
|
||||||
| `zod-4` | Zod 4 API patterns |
|
| `zod-4` | Zod 4 API patterns |
|
||||||
|
|||||||
371
skills/tdd/SKILL.md
Normal file
371
skills/tdd/SKILL.md
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
---
|
||||||
|
name: tdd
|
||||||
|
description: >
|
||||||
|
Test-Driven Development workflow for ALL Prowler components (UI, SDK, API).
|
||||||
|
Trigger: ALWAYS when implementing features, fixing bugs, or refactoring - regardless of component.
|
||||||
|
This is a MANDATORY workflow, not optional.
|
||||||
|
license: Apache-2.0
|
||||||
|
metadata:
|
||||||
|
author: prowler-cloud
|
||||||
|
version: "2.0"
|
||||||
|
scope: [root, ui, api, prowler]
|
||||||
|
auto_invoke:
|
||||||
|
- "Implementing feature"
|
||||||
|
- "Fixing bug"
|
||||||
|
- "Refactoring code"
|
||||||
|
- "Working on task"
|
||||||
|
- "Modifying component"
|
||||||
|
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||||
|
---
|
||||||
|
|
||||||
|
## TDD Cycle (MANDATORY)
|
||||||
|
|
||||||
|
```
|
||||||
|
+-----------------------------------------+
|
||||||
|
| RED -> GREEN -> REFACTOR |
|
||||||
|
| ^ | |
|
||||||
|
| +------------------------+ |
|
||||||
|
+-----------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
**The question is NOT "should I write tests?" but "what tests do I need?"**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The Three Laws of TDD
|
||||||
|
|
||||||
|
1. **No production code** until you have a failing test
|
||||||
|
2. **No more test** than necessary to fail
|
||||||
|
3. **No more code** than necessary to pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detect Your Stack
|
||||||
|
|
||||||
|
Before starting, identify which component you're working on:
|
||||||
|
|
||||||
|
| Working in | Stack | Runner | Test pattern | Details |
|
||||||
|
|------------|-------|--------|-------------|---------|
|
||||||
|
| `ui/` | TypeScript / React | Vitest + RTL | `*.test.{ts,tsx}` (co-located) | See `vitest` skill |
|
||||||
|
| `prowler/` | Python | pytest + moto | `*_test.py` (suffix) in `tests/` | See `prowler-test-sdk` skill |
|
||||||
|
| `api/` | Python / Django | pytest + django | `test_*.py` (prefix) in `api/src/backend/**/tests/` | See `prowler-test-api` skill |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0: Assessment (ALWAYS FIRST)
|
||||||
|
|
||||||
|
Before writing ANY code:
|
||||||
|
|
||||||
|
### UI (`ui/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find existing tests
|
||||||
|
fd "*.test.tsx" ui/components/feature/
|
||||||
|
|
||||||
|
# 2. Check coverage
|
||||||
|
pnpm test:coverage -- components/feature/
|
||||||
|
|
||||||
|
# 3. Read existing tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### SDK (`prowler/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find existing tests
|
||||||
|
fd "*_test.py" tests/providers/aws/services/ec2/
|
||||||
|
|
||||||
|
# 2. Run specific test
|
||||||
|
poetry run pytest tests/providers/aws/services/ec2/ec2_ami_public/ -v
|
||||||
|
|
||||||
|
# 3. Read existing tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### API (`api/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Find existing tests
|
||||||
|
fd "test_*.py" api/src/backend/api/tests/
|
||||||
|
|
||||||
|
# 2. Run specific test
|
||||||
|
poetry run pytest api/src/backend/api/tests/test_models.py -v
|
||||||
|
|
||||||
|
# 3. Read existing tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decision Tree (All Stacks)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------+
|
||||||
|
| Does test file exist for this code? |
|
||||||
|
+----------+-----------------------+-------+
|
||||||
|
| NO | YES
|
||||||
|
v v
|
||||||
|
+------------------+ +------------------+
|
||||||
|
| CREATE test file | | Check coverage |
|
||||||
|
| -> Phase 1: RED | | for your change |
|
||||||
|
+------------------+ +--------+---------+
|
||||||
|
|
|
||||||
|
+--------+--------+
|
||||||
|
| Missing cases? |
|
||||||
|
+---+---------+---+
|
||||||
|
| YES | NO
|
||||||
|
v v
|
||||||
|
+-----------+ +-----------+
|
||||||
|
| ADD tests | | Proceed |
|
||||||
|
| Phase 1 | | Phase 2 |
|
||||||
|
+-----------+ +-----------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: RED - Write Failing Tests
|
||||||
|
|
||||||
|
### For NEW Functionality
|
||||||
|
|
||||||
|
**UI (Vitest)**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe("PriceCalculator", () => {
|
||||||
|
it("should return 0 for quantities below threshold", () => {
|
||||||
|
// Given
|
||||||
|
const quantity = 3;
|
||||||
|
|
||||||
|
// When
|
||||||
|
const result = calculateDiscount(quantity);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(result).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**SDK (pytest)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Test_ec2_ami_public:
|
||||||
|
@mock_aws
|
||||||
|
def test_no_public_amis(self):
|
||||||
|
# Given - No AMIs exist
|
||||||
|
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||||
|
|
||||||
|
with mock.patch("prowler...ec2_service", new=EC2(aws_provider)):
|
||||||
|
from prowler...ec2_ami_public import ec2_ami_public
|
||||||
|
|
||||||
|
# When
|
||||||
|
check = ec2_ami_public()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert len(result) == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**API (pytest-django)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestResourceModel:
|
||||||
|
def test_create_resource_with_tags(self, providers_fixture):
|
||||||
|
# Given
|
||||||
|
provider, *_ = providers_fixture
|
||||||
|
tenant_id = provider.tenant_id
|
||||||
|
|
||||||
|
# When
|
||||||
|
resource = Resource.objects.create(
|
||||||
|
tenant_id=tenant_id, provider=provider,
|
||||||
|
uid="arn:aws:ec2:us-east-1:123456789:instance/i-1234",
|
||||||
|
name="test", region="us-east-1", service="ec2", type="instance",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
assert resource.uid == "arn:aws:ec2:us-east-1:123456789:instance/i-1234"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run -> MUST fail:** Test references code that doesn't exist yet.
|
||||||
|
|
||||||
|
### For BUG FIXES
|
||||||
|
|
||||||
|
Write a test that **reproduces the bug** first:
|
||||||
|
|
||||||
|
**UI:** `expect(() => render(<DatePicker value={null} />)).not.toThrow();`
|
||||||
|
|
||||||
|
**SDK:** `assert result[0].status == "FAIL" # Currently returns PASS incorrectly`
|
||||||
|
|
||||||
|
**API:** `assert response.status_code == 403 # Currently returns 200`
|
||||||
|
|
||||||
|
**Run -> Should FAIL (reproducing the bug)**
|
||||||
|
|
||||||
|
### For REFACTORING
|
||||||
|
|
||||||
|
Capture ALL current behavior BEFORE refactoring:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Any stack: run ALL existing tests, they should PASS
|
||||||
|
# This is your safety net - if any fail after refactoring, you broke something
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run -> All should PASS (baseline)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: GREEN - Minimum Code
|
||||||
|
|
||||||
|
Write the MINIMUM code to make the test pass. Hardcoding is valid for the first test.
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Test expects calculateDiscount(100, 10) === 10
|
||||||
|
function calculateDiscount() {
|
||||||
|
return 10; // FAKE IT - hardcoded is valid for first test
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python (SDK/API):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Test expects check.execute() returns 0 results
|
||||||
|
def execute(self):
|
||||||
|
return [] # FAKE IT - hardcoded is valid for first test
|
||||||
|
```
|
||||||
|
|
||||||
|
**This passes. But we're not done...**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Triangulation (CRITICAL)
|
||||||
|
|
||||||
|
**One test allows faking. Multiple tests FORCE real logic.**
|
||||||
|
|
||||||
|
Add tests with different inputs that break the hardcoded value:
|
||||||
|
|
||||||
|
| Scenario | Required? |
|
||||||
|
|----------|-----------|
|
||||||
|
| Happy path | YES |
|
||||||
|
| Zero/empty values | YES |
|
||||||
|
| Boundary values | YES |
|
||||||
|
| Different valid inputs | YES (breaks fake) |
|
||||||
|
| Error conditions | YES |
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it("should calculate 10% discount", () => {
|
||||||
|
expect(calculateDiscount(100, 10)).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ADD - breaks the fake:
|
||||||
|
it("should calculate 15% on 200", () => {
|
||||||
|
expect(calculateDiscount(200, 15)).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 for 0% rate", () => {
|
||||||
|
expect(calculateDiscount(100, 0)).toBe(0);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_single_public_ami(self):
|
||||||
|
# Different input -> breaks hardcoded empty list
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
|
||||||
|
def test_private_ami(self):
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Now fake BREAKS -> Real implementation required.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: REFACTOR
|
||||||
|
|
||||||
|
Tests GREEN -> Improve code quality WITHOUT changing behavior.
|
||||||
|
|
||||||
|
- Extract functions/methods
|
||||||
|
- Improve naming
|
||||||
|
- Add types/validation
|
||||||
|
- Reduce duplication
|
||||||
|
|
||||||
|
**Run tests after EACH change -> Must stay GREEN**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------+
|
||||||
|
| TDD WORKFLOW |
|
||||||
|
+------------------------------------------------+
|
||||||
|
| 0. ASSESS: What tests exist? What's missing? |
|
||||||
|
| |
|
||||||
|
| 1. RED: Write ONE failing test |
|
||||||
|
| +-- Run -> Must fail with clear error |
|
||||||
|
| |
|
||||||
|
| 2. GREEN: Write MINIMUM code to pass |
|
||||||
|
| +-- Fake It is valid for first test |
|
||||||
|
| |
|
||||||
|
| 3. TRIANGULATE: Add tests that break the fake |
|
||||||
|
| +-- Different inputs, edge cases |
|
||||||
|
| |
|
||||||
|
| 4. REFACTOR: Improve with confidence |
|
||||||
|
| +-- Tests stay green throughout |
|
||||||
|
| |
|
||||||
|
| 5. REPEAT: Next behavior/requirement |
|
||||||
|
+------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anti-Patterns (NEVER DO)
|
||||||
|
|
||||||
|
```
|
||||||
|
# ANY language:
|
||||||
|
|
||||||
|
# 1. Code first, tests after
|
||||||
|
def new_feature(): ... # Then writing tests = USELESS
|
||||||
|
|
||||||
|
# 2. Skip triangulation
|
||||||
|
# Single test allows faking forever
|
||||||
|
|
||||||
|
# 3. Test implementation details
|
||||||
|
assert component.state.is_loading == True # BAD - test behavior, not internals
|
||||||
|
assert mock_service.call_count == 3 # BAD - brittle coupling
|
||||||
|
|
||||||
|
# 4. All tests at once before any code
|
||||||
|
# Write ONE test, make it pass, THEN write the next
|
||||||
|
|
||||||
|
# 5. Giant test methods
|
||||||
|
# Each test should verify ONE behavior
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands by Stack
|
||||||
|
|
||||||
|
### UI (`ui/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test # Watch mode
|
||||||
|
pnpm test:run # Single run (CI)
|
||||||
|
pnpm test:coverage # Coverage report
|
||||||
|
pnpm test ComponentName # Filter by name
|
||||||
|
```
|
||||||
|
|
||||||
|
### SDK (`prowler/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run pytest tests/path/ -v # Run specific tests
|
||||||
|
poetry run pytest tests/path/ -v -k "test_name" # Filter by name
|
||||||
|
poetry run pytest -n auto tests/ # Parallel run
|
||||||
|
poetry run pytest --cov=./prowler tests/ # Coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
### API (`api/`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry run pytest -x --tb=short # Run all (stop on first fail)
|
||||||
|
poetry run pytest api/src/backend/api/tests/test_file.py # Specific file
|
||||||
|
poetry run pytest -k "test_name" -v # Filter by name
|
||||||
|
```
|
||||||
201
skills/vitest/SKILL.md
Normal file
201
skills/vitest/SKILL.md
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
---
|
||||||
|
name: vitest
|
||||||
|
description: >
|
||||||
|
Vitest unit testing patterns with React Testing Library.
|
||||||
|
Trigger: When writing unit tests for React components, hooks, or utilities.
|
||||||
|
license: Apache-2.0
|
||||||
|
metadata:
|
||||||
|
author: prowler-cloud
|
||||||
|
version: "1.0"
|
||||||
|
scope: [root, ui]
|
||||||
|
auto_invoke:
|
||||||
|
- "Writing Vitest tests"
|
||||||
|
- "Writing React component tests"
|
||||||
|
- "Writing unit tests for UI"
|
||||||
|
- "Testing hooks or utilities"
|
||||||
|
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, Task
|
||||||
|
---
|
||||||
|
|
||||||
|
> **For E2E tests**: Use `prowler-test-ui` skill (Playwright).
|
||||||
|
> This skill covers **unit/integration tests** with Vitest + React Testing Library.
|
||||||
|
|
||||||
|
## Test Structure (REQUIRED)
|
||||||
|
|
||||||
|
Use **Given/When/Then** (AAA) pattern with comments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it("should update user name when form is submitted", async () => {
|
||||||
|
// Given - Arrange
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const onSubmit = vi.fn();
|
||||||
|
render(<UserForm onSubmit={onSubmit} />);
|
||||||
|
|
||||||
|
// When - Act
|
||||||
|
await user.type(screen.getByLabelText(/name/i), "John");
|
||||||
|
await user.click(screen.getByRole("button", { name: /submit/i }));
|
||||||
|
|
||||||
|
// Then - Assert
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({ name: "John" });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Describe Block Organization
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe("ComponentName", () => {
|
||||||
|
describe("when [condition]", () => {
|
||||||
|
it("should [expected behavior]", () => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Group by behavior, NOT by method.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Query Priority (REQUIRED)
|
||||||
|
|
||||||
|
| Priority | Query | Use Case |
|
||||||
|
|----------|-------|----------|
|
||||||
|
| 1 | `getByRole` | Buttons, inputs, headings |
|
||||||
|
| 2 | `getByLabelText` | Form fields |
|
||||||
|
| 3 | `getByPlaceholderText` | Inputs without label |
|
||||||
|
| 4 | `getByText` | Static text |
|
||||||
|
| 5 | `getByTestId` | Last resort only |
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ GOOD
|
||||||
|
screen.getByRole("button", { name: /submit/i });
|
||||||
|
screen.getByLabelText(/email/i);
|
||||||
|
|
||||||
|
// ❌ BAD
|
||||||
|
container.querySelector(".btn-primary");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## userEvent over fireEvent (REQUIRED)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ ALWAYS use userEvent
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.click(button);
|
||||||
|
await user.type(input, "hello");
|
||||||
|
|
||||||
|
// ❌ NEVER use fireEvent for interactions
|
||||||
|
fireEvent.click(button);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Async Testing Patterns
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ findBy for elements that appear async
|
||||||
|
const element = await screen.findByText(/loaded/i);
|
||||||
|
|
||||||
|
// ✅ waitFor for assertions
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/success/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ✅ ONE assertion per waitFor
|
||||||
|
await waitFor(() => expect(mockFn).toHaveBeenCalled());
|
||||||
|
await waitFor(() => expect(screen.getByText(/done/i)).toBeVisible());
|
||||||
|
|
||||||
|
// ❌ NEVER multiple assertions in waitFor
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockFn).toHaveBeenCalled();
|
||||||
|
expect(screen.getByText(/done/i)).toBeVisible(); // Slower failures
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mocking
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic mock
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
|
||||||
|
// Mock with return value
|
||||||
|
const fetchUser = vi.fn().mockResolvedValue({ name: "John" });
|
||||||
|
|
||||||
|
// Always clean up
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### vi.spyOn vs vi.mock
|
||||||
|
|
||||||
|
| Method | When to Use |
|
||||||
|
|--------|-------------|
|
||||||
|
| `vi.spyOn` | Observe without replacing (PREFERRED) |
|
||||||
|
| `vi.mock` | Replace entire module (use sparingly) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Matchers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Presence
|
||||||
|
expect(element).toBeInTheDocument();
|
||||||
|
expect(element).toBeVisible();
|
||||||
|
|
||||||
|
// State
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
expect(input).toHaveValue("text");
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
// Content
|
||||||
|
expect(element).toHaveTextContent(/hello/i);
|
||||||
|
expect(element).toHaveAttribute("href", "/home");
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
expect(fn).toHaveBeenCalledWith(arg1, arg2);
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What NOT to Test
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ❌ Internal state
|
||||||
|
expect(component.state.isLoading).toBe(true);
|
||||||
|
|
||||||
|
// ❌ Third-party libraries
|
||||||
|
expect(axios.get).toHaveBeenCalled();
|
||||||
|
|
||||||
|
// ❌ Static content (unless conditional)
|
||||||
|
expect(screen.getByText("Welcome")).toBeInTheDocument();
|
||||||
|
|
||||||
|
// ✅ User-visible behavior
|
||||||
|
expect(screen.getByRole("button")).toBeDisabled();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
components/
|
||||||
|
├── Button/
|
||||||
|
│ ├── Button.tsx
|
||||||
|
│ ├── Button.test.tsx # Co-located
|
||||||
|
│ └── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test # Watch mode
|
||||||
|
pnpm test:run # Single run
|
||||||
|
pnpm test:coverage # With coverage
|
||||||
|
pnpm test Button # Filter by name
|
||||||
|
```
|
||||||
@@ -159,6 +159,61 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Run unit tests (targeted based on staged files)
|
||||||
|
echo -e "${BLUE}🧪 Running unit tests...${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Get staged source files (exclude test files)
|
||||||
|
# Note: we already cd'd into ui/, so pathspecs are relative (no ui/ prefix)
|
||||||
|
STAGED_SOURCE_FILES=$(git diff --cached --name-only --diff-filter=ACM -- '*.ts' '*.tsx' | grep -v '\.test\.\|\.spec\.\|vitest\.config\|vitest\.setup' || true)
|
||||||
|
|
||||||
|
# Check if critical paths changed (lib/, types/, config/)
|
||||||
|
CRITICAL_PATHS_CHANGED=$(git diff --cached --name-only -- 'lib/**' 'types/**' 'config/**' 'middleware.ts' 'vitest.config.ts' 'vitest.setup.ts' || true)
|
||||||
|
|
||||||
|
if [ -n "$CRITICAL_PATHS_CHANGED" ]; then
|
||||||
|
echo -e "${YELLOW}Critical paths changed - running ALL unit tests${NC}"
|
||||||
|
if pnpm run test:run; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Unit tests passed${NC}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}❌ Unit tests failed${NC}"
|
||||||
|
echo -e "${RED}Fix failing tests before committing${NC}"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ -n "$STAGED_SOURCE_FILES" ]; then
|
||||||
|
echo -e "${YELLOW}Running tests related to changed files:${NC}"
|
||||||
|
echo "$STAGED_SOURCE_FILES" | while IFS= read -r file; do [ -n "$file" ] && echo " - $file"; done
|
||||||
|
echo ""
|
||||||
|
# shellcheck disable=SC2086 # Word splitting is intentional - vitest needs each file as separate arg
|
||||||
|
if pnpm exec vitest related $STAGED_SOURCE_FILES --run; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Unit tests passed${NC}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}❌ Unit tests failed${NC}"
|
||||||
|
echo -e "${RED}Fix failing tests before committing${NC}"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}No source files changed - running ALL unit tests${NC}"
|
||||||
|
if pnpm run test:run; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✅ Unit tests passed${NC}"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}❌ Unit tests failed${NC}"
|
||||||
|
echo -e "${RED}Fix failing tests before committing${NC}"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Run build
|
# Run build
|
||||||
echo -e "${BLUE}🔨 Running build...${NC}"
|
echo -e "${BLUE}🔨 Running build...${NC}"
|
||||||
echo ""
|
echo ""
|
||||||
|
|||||||
11
ui/AGENTS.md
11
ui/AGENTS.md
@@ -11,6 +11,8 @@
|
|||||||
> - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware
|
> - [`zustand-5`](../skills/zustand-5/SKILL.md) - Selectors, persist middleware
|
||||||
> - [`ai-sdk-5`](../skills/ai-sdk-5/SKILL.md) - UIMessage, sendMessage
|
> - [`ai-sdk-5`](../skills/ai-sdk-5/SKILL.md) - UIMessage, sendMessage
|
||||||
> - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors
|
> - [`playwright`](../skills/playwright/SKILL.md) - Page Object Model, selectors
|
||||||
|
> - [`vitest`](../skills/vitest/SKILL.md) - Unit testing with React Testing Library
|
||||||
|
> - [`tdd`](../skills/tdd/SKILL.md) - TDD workflow (MANDATORY for UI tasks)
|
||||||
|
|
||||||
### Auto-invoke Skills
|
### Auto-invoke Skills
|
||||||
|
|
||||||
@@ -26,16 +28,25 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST:
|
|||||||
| Creating Zod schemas | `zod-4` |
|
| Creating Zod schemas | `zod-4` |
|
||||||
| Creating a git commit | `prowler-commit` |
|
| Creating a git commit | `prowler-commit` |
|
||||||
| Creating/modifying Prowler UI components | `prowler-ui` |
|
| Creating/modifying Prowler UI components | `prowler-ui` |
|
||||||
|
| Fixing bug | `tdd` |
|
||||||
|
| Implementing feature | `tdd` |
|
||||||
|
| Modifying component | `tdd` |
|
||||||
|
| Refactoring code | `tdd` |
|
||||||
| Review changelog format and conventions | `prowler-changelog` |
|
| Review changelog format and conventions | `prowler-changelog` |
|
||||||
|
| Testing hooks or utilities | `vitest` |
|
||||||
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
| Update CHANGELOG.md in any component | `prowler-changelog` |
|
||||||
| Using Zustand stores | `zustand-5` |
|
| Using Zustand stores | `zustand-5` |
|
||||||
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
| Working on Prowler UI structure (actions/adapters/types/hooks) | `prowler-ui` |
|
||||||
|
| Working on task | `tdd` |
|
||||||
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
| Working with Prowler UI test helpers/pages | `prowler-test-ui` |
|
||||||
| Working with Tailwind classes | `tailwind-4` |
|
| Working with Tailwind classes | `tailwind-4` |
|
||||||
| Writing Playwright E2E tests | `playwright` |
|
| Writing Playwright E2E tests | `playwright` |
|
||||||
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
| Writing Prowler UI E2E tests | `prowler-test-ui` |
|
||||||
|
| Writing React component tests | `vitest` |
|
||||||
| Writing React components | `react-19` |
|
| Writing React components | `react-19` |
|
||||||
| Writing TypeScript types/interfaces | `typescript` |
|
| Writing TypeScript types/interfaces | `typescript` |
|
||||||
|
| Writing Vitest tests | `vitest` |
|
||||||
|
| Writing unit tests for UI | `vitest` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
|||||||
|
|
||||||
## [1.18.0] (Prowler v5.18.0)
|
## [1.18.0] (Prowler v5.18.0)
|
||||||
|
|
||||||
|
### 🚀 Added
|
||||||
|
|
||||||
|
- Setup Vitest with React Testing Library for unit testing with targeted test execution [(#9925)](https://github.com/prowler-cloud/prowler/pull/9925)
|
||||||
|
|
||||||
### 🔄 Changed
|
### 🔄 Changed
|
||||||
|
|
||||||
- Restyle resources view with improved resource detail drawer [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864)
|
- Restyle resources view with improved resource detail drawer [(#9864)](https://github.com/prowler-cloud/prowler/pull/9864)
|
||||||
|
|||||||
61
ui/components/ui/button/button.test.tsx
Normal file
61
ui/components/ui/button/button.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { Button } from "./button";
|
||||||
|
|
||||||
|
describe("Button", () => {
|
||||||
|
it("renders with children", () => {
|
||||||
|
render(<Button>Click me</Button>);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Click me" }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles click events", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const handleClick = vi.fn();
|
||||||
|
|
||||||
|
render(<Button onClick={handleClick}>Click me</Button>);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button"));
|
||||||
|
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can be disabled", () => {
|
||||||
|
render(<Button disabled>Disabled</Button>);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button")).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies variant classes correctly", () => {
|
||||||
|
const { rerender } = render(<Button variant="destructive">Delete</Button>);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button")).toHaveClass("bg-destructive");
|
||||||
|
|
||||||
|
rerender(<Button variant="outline">Cancel</Button>);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button")).toHaveClass("border");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies size classes correctly", () => {
|
||||||
|
render(<Button size="sm">Small</Button>);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button")).toHaveClass("h-8");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders as child component when asChild is true", () => {
|
||||||
|
render(
|
||||||
|
<Button asChild>
|
||||||
|
<a href="/test">Link Button</a>
|
||||||
|
</Button>,
|
||||||
|
);
|
||||||
|
|
||||||
|
const link = screen.getByRole("link", { name: "Link Button" });
|
||||||
|
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
expect(link).toHaveAttribute("href", "/test");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -695,6 +695,30 @@
|
|||||||
"strategy": "installed",
|
"strategy": "installed",
|
||||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "@testing-library/jest-dom",
|
||||||
|
"from": "6.9.1",
|
||||||
|
"to": "6.9.1",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "@testing-library/react",
|
||||||
|
"from": "16.3.2",
|
||||||
|
"to": "16.3.2",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "@testing-library/user-event",
|
||||||
|
"from": "14.6.1",
|
||||||
|
"to": "14.6.1",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "devDependencies",
|
"section": "devDependencies",
|
||||||
"name": "@types/d3",
|
"name": "@types/d3",
|
||||||
@@ -775,6 +799,22 @@
|
|||||||
"strategy": "installed",
|
"strategy": "installed",
|
||||||
"generatedAt": "2026-01-19T13:54:24.770Z"
|
"generatedAt": "2026-01-19T13:54:24.770Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "@vitejs/plugin-react",
|
||||||
|
"from": "5.1.2",
|
||||||
|
"to": "5.1.2",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "@vitest/coverage-v8",
|
||||||
|
"from": "4.0.18",
|
||||||
|
"to": "4.0.18",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "devDependencies",
|
"section": "devDependencies",
|
||||||
"name": "autoprefixer",
|
"name": "autoprefixer",
|
||||||
@@ -911,6 +951,14 @@
|
|||||||
"strategy": "installed",
|
"strategy": "installed",
|
||||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "jsdom",
|
||||||
|
"from": "27.4.0",
|
||||||
|
"to": "27.4.0",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"section": "devDependencies",
|
"section": "devDependencies",
|
||||||
"name": "lint-staged",
|
"name": "lint-staged",
|
||||||
@@ -974,5 +1022,13 @@
|
|||||||
"to": "5.5.4",
|
"to": "5.5.4",
|
||||||
"strategy": "installed",
|
"strategy": "installed",
|
||||||
"generatedAt": "2025-10-22T12:36:37.962Z"
|
"generatedAt": "2025-10-22T12:36:37.962Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": "devDependencies",
|
||||||
|
"name": "vitest",
|
||||||
|
"from": "4.0.18",
|
||||||
|
"to": "4.0.18",
|
||||||
|
"strategy": "installed",
|
||||||
|
"generatedAt": "2026-01-29T16:42:27.795Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
"format:check": "./node_modules/.bin/prettier --check ./app",
|
"format:check": "./node_modules/.bin/prettier --check ./app",
|
||||||
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
|
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
"test:e2e": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans",
|
"test:e2e": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans",
|
||||||
"test:e2e:ui": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --ui",
|
"test:e2e:ui": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --ui",
|
||||||
"test:e2e:debug": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --debug",
|
"test:e2e:debug": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --debug",
|
||||||
@@ -113,6 +116,9 @@
|
|||||||
"@iconify/react": "5.2.1",
|
"@iconify/react": "5.2.1",
|
||||||
"@next/eslint-plugin-next": "16.1.6",
|
"@next/eslint-plugin-next": "16.1.6",
|
||||||
"@playwright/test": "1.56.1",
|
"@playwright/test": "1.56.1",
|
||||||
|
"@testing-library/jest-dom": "6.9.1",
|
||||||
|
"@testing-library/react": "16.3.2",
|
||||||
|
"@testing-library/user-event": "14.6.1",
|
||||||
"@types/d3": "7.4.3",
|
"@types/d3": "7.4.3",
|
||||||
"@types/geojson": "7946.0.16",
|
"@types/geojson": "7946.0.16",
|
||||||
"@types/node": "24.10.8",
|
"@types/node": "24.10.8",
|
||||||
@@ -123,6 +129,8 @@
|
|||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.53.0",
|
"@typescript-eslint/eslint-plugin": "8.53.0",
|
||||||
"@typescript-eslint/parser": "8.53.0",
|
"@typescript-eslint/parser": "8.53.0",
|
||||||
|
"@vitejs/plugin-react": "5.1.2",
|
||||||
|
"@vitest/coverage-v8": "4.0.18",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"dotenv-expand": "12.0.3",
|
"dotenv-expand": "12.0.3",
|
||||||
@@ -140,6 +148,7 @@
|
|||||||
"eslint-plugin-unused-imports": "4.3.0",
|
"eslint-plugin-unused-imports": "4.3.0",
|
||||||
"globals": "17.0.0",
|
"globals": "17.0.0",
|
||||||
"husky": "9.1.7",
|
"husky": "9.1.7",
|
||||||
|
"jsdom": "27.4.0",
|
||||||
"lint-staged": "15.5.2",
|
"lint-staged": "15.5.2",
|
||||||
"postcss": "8.4.38",
|
"postcss": "8.4.38",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
@@ -147,7 +156,8 @@
|
|||||||
"shadcn": "3.8.4",
|
"shadcn": "3.8.4",
|
||||||
"tailwind-variants": "0.1.20",
|
"tailwind-variants": "0.1.20",
|
||||||
"tailwindcss": "4.1.18",
|
"tailwindcss": "4.1.18",
|
||||||
"typescript": "5.5.4"
|
"typescript": "5.5.4",
|
||||||
|
"vitest": "4.0.18"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
1267
ui/pnpm-lock.yaml
generated
1267
ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,11 @@
|
|||||||
"target": "es5"
|
"target": "es5"
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.setup.ts",
|
||||||
|
"**/*.test.ts",
|
||||||
|
"**/*.test.tsx"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|||||||
35
ui/vitest.config.ts
Normal file
35
ui/vitest.config.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "path";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
globals: true,
|
||||||
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
|
include: ["**/*.test.{ts,tsx}"],
|
||||||
|
exclude: [
|
||||||
|
"node_modules",
|
||||||
|
".next",
|
||||||
|
"tests/**/*", // Playwright E2E tests
|
||||||
|
],
|
||||||
|
coverage: {
|
||||||
|
provider: "v8",
|
||||||
|
reporter: ["text", "json", "html"],
|
||||||
|
exclude: [
|
||||||
|
"node_modules",
|
||||||
|
".next",
|
||||||
|
"tests/**/*",
|
||||||
|
"**/*.test.{ts,tsx}",
|
||||||
|
"vitest.config.ts",
|
||||||
|
"vitest.setup.ts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
1
ui/vitest.setup.ts
Normal file
1
ui/vitest.setup.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "@testing-library/jest-dom/vitest";
|
||||||
Reference in New Issue
Block a user