diff --git a/ui/.husky/pre-commit b/ui/.husky/pre-commit deleted file mode 100755 index 0755cd1872..0000000000 --- a/ui/.husky/pre-commit +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/bash - -# Prowler UI - Pre-Commit Hook -# Optionally validates ONLY staged files against AGENTS.md standards using Claude Code -# Controlled by CODE_REVIEW_ENABLED in .env - -set -e - -# The Python pre-commit framework (see .pre-commit-config.yaml, hook "ui-checks") -# exports GIT_WORK_TREE, GIT_DIR, and GIT_INDEX_FILE pointing to its temp staging -# area. Unset them so git commands below resolve against the real repo and index. -# See: https://github.com/prowler-cloud/prowler/pull/10574 -unset GIT_WORK_TREE GIT_DIR GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "🚀 Prowler UI - Pre-Commit Hook" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" - -# Load .env file (look in git root directory) -GIT_ROOT=$(git rev-parse --show-toplevel) -if [ -f "$GIT_ROOT/ui/.env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/ui/.env" | cut -d'=' -f2 | tr -d ' ') -elif [ -f "$GIT_ROOT/.env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" "$GIT_ROOT/.env" | cut -d'=' -f2 | tr -d ' ') -elif [ -f ".env" ]; then - CODE_REVIEW_ENABLED=$(grep "^CODE_REVIEW_ENABLED" .env | cut -d'=' -f2 | tr -d ' ') -else - CODE_REVIEW_ENABLED="false" -fi - -# Normalize the value to lowercase -CODE_REVIEW_ENABLED=$(echo "$CODE_REVIEW_ENABLED" | tr '[:upper:]' '[:lower:]') - -echo -e "${BLUE}ℹ️ Code Review Status: ${CODE_REVIEW_ENABLED}${NC}" -echo "" - -# Get staged files in the UI folder only (what will be committed) -# Always use GIT_ROOT-relative pathspecs so detection works regardless of cwd -STAGED_FILES=$(git -C "$GIT_ROOT" diff --cached --name-only --diff-filter=ACM -- 'ui/' | grep -E '\.(tsx?|jsx?)$' || true) - -if [ "$CODE_REVIEW_ENABLED" = "true" ]; then - if [ -z "$STAGED_FILES" ]; then - echo -e "${YELLOW}⚠️ No TypeScript/JavaScript files staged to validate${NC}" - echo "" - else - echo -e "${YELLOW}🔍 Running Claude Code standards validation...${NC}" - echo "" - echo -e "${BLUE}📋 Files to validate:${NC}" - echo "$STAGED_FILES" | while IFS= read -r file; do echo " - $file"; done - echo "" - - echo -e "${BLUE}📤 Sending to Claude Code for validation...${NC}" - echo "" - - # Build prompt with full file contents - VALIDATION_PROMPT=$( - cat <<'PROMPT_EOF' -You are a code reviewer for the Prowler UI project. Analyze the full file contents of changed files below and validate they comply with AGENTS.md standards. - -**RULES TO CHECK:** -1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }` -2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const` -3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs. -4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")` -5. React 19: NO `useMemo`/`useCallback` without reason -6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod. -7. File Org: 1 feature = local, 2+ features = shared -8. Directives: Server Actions need "use server", clients need "use client" -9. Implement DRY, KISS principles. (example: reusable components, avoid repetition) -10. Layout must work for all the responsive breakpoints (mobile, tablet, desktop) -11. ANY types cannot be used - CRITICAL: Check for `: any` in all visible lines -12. Use the components inside components/shadcn if possible -13. Check Accessibility best practices (like alt tags in images, semantic HTML, Aria labels, etc.) - -=== FILES TO REVIEW === -PROMPT_EOF - ) - - # Add full file contents for each staged file - for file in $STAGED_FILES; do - VALIDATION_PROMPT="$VALIDATION_PROMPT - -=== FILE: $file === -$(cat "$file" 2>/dev/null || echo "Error reading file")" - done - - VALIDATION_PROMPT="$VALIDATION_PROMPT - -=== END FILES === - -**IMPORTANT: Your response MUST start with exactly one of these lines:** -STATUS: PASSED -STATUS: FAILED - -**If FAILED:** List each violation with File, Line Number, Rule Number, and Issue. -**If PASSED:** Confirm all files comply with AGENTS.md standards. - -**Start your response now with STATUS:**" - - # Send to Claude Code - if VALIDATION_OUTPUT=$(echo "$VALIDATION_PROMPT" | claude 2>&1); then - echo "$VALIDATION_OUTPUT" - echo "" - - # Check result - STRICT MODE: fail if status unclear - if echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: PASSED"; then - echo "" - echo -e "${GREEN}✅ VALIDATION PASSED${NC}" - echo "" - elif echo "$VALIDATION_OUTPUT" | grep -q "^STATUS: FAILED"; then - echo "" - echo -e "${RED}❌ VALIDATION FAILED${NC}" - echo -e "${RED}Fix violations before committing${NC}" - echo "" - exit 1 - else - echo "" - echo -e "${RED}❌ VALIDATION ERROR${NC}" - echo -e "${RED}Could not determine validation status from Claude Code response${NC}" - echo -e "${YELLOW}Response must start with 'STATUS: PASSED' or 'STATUS: FAILED'${NC}" - echo "" - echo -e "${YELLOW}To bypass validation temporarily, set CODE_REVIEW_ENABLED=false in .env${NC}" - echo "" - exit 1 - fi - else - echo -e "${YELLOW}⚠️ Claude Code not available${NC}" - fi - echo "" - fi -else - echo -e "${YELLOW}⏭️ Code review disabled (CODE_REVIEW_ENABLED=false)${NC}" - echo "" -fi - -# Run healthcheck (typecheck and lint check) only if there are UI changes -if [ -z "$STAGED_FILES" ]; then - echo -e "${YELLOW}⏭️ No UI files staged, skipping healthcheck/tests/build${NC}" - echo "" - exit 0 -fi - -echo -e "${BLUE}🏥 Running healthcheck...${NC}" -echo "" - -cd "$GIT_ROOT/ui" -if pnpm run healthcheck; then - echo "" - echo -e "${GREEN}✅ Healthcheck passed${NC}" - echo "" -else - echo "" - echo -e "${RED}❌ Healthcheck failed${NC}" - echo -e "${RED}Fix type errors and linting issues before committing${NC}" - echo "" - exit 1 -fi - -# Run unit tests (targeted based on staged files) -echo -e "${BLUE}🧪 Running unit tests...${NC}" -echo "" - -# Get staged source files (exclude test files) -# Use GIT_ROOT so pathspecs are always correct regardless of cwd -STAGED_SOURCE_FILES=$(git -C "$GIT_ROOT" diff --cached --name-only --diff-filter=ACM -- 'ui/*.ts' 'ui/*.tsx' | sed 's|^ui/||' | grep -v '\.test\.\|\.spec\.\|vitest\.config\|vitest\.setup' || true) - -# Check if critical paths changed (lib/, types/, config/) -CRITICAL_PATHS_CHANGED=$(git -C "$GIT_ROOT" diff --cached --name-only -- 'ui/lib/' 'ui/types/' 'ui/config/' 'ui/middleware.ts' 'ui/vitest.config.ts' 'ui/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 -echo -e "${BLUE}🔨 Running build...${NC}" -echo "" - -if pnpm run build; then - echo "" - echo -e "${GREEN}✅ Build passed${NC}" - echo "" -else - echo "" - echo -e "${RED}❌ Build failed${NC}" - echo -e "${RED}Fix build errors before committing${NC}" - echo "" - exit 1 -fi diff --git a/ui/.pre-commit-config.yaml b/ui/.pre-commit-config.yaml index c92ea6cfd3..eab333ceea 100644 --- a/ui/.pre-commit-config.yaml +++ b/ui/.pre-commit-config.yaml @@ -3,21 +3,30 @@ orphan: true repos: - repo: local hooks: + # P0 - Formatters: write fixes on staged files; prek re-stages. + - id: ui-prettier + name: UI - Prettier (write, staged) + entry: pnpm exec prettier --write --ignore-unknown + language: system + pass_filenames: true + priority: 0 + + - id: ui-lint + name: UI - ESLint (fix, staged) + entry: pnpm exec eslint --fix --max-warnings 40 --no-warn-ignored + language: system + files: '\.(ts|tsx|js|jsx)$' + pass_filenames: true + priority: 1 + + # P10 - Project-wide validators (TypeScript is fundamentally project-wide). - id: ui-typecheck name: UI - TypeScript Check entry: pnpm run typecheck language: system files: '\.(ts|tsx|js|jsx)$' pass_filenames: false - priority: 0 - - - id: ui-lint - name: UI - ESLint - entry: pnpm run lint:check - language: system - files: '\.(ts|tsx|js|jsx)$' - pass_filenames: false - priority: 0 + priority: 10 - id: ui-tests name: UI - Unit Tests @@ -26,12 +35,4 @@ repos: files: '\.(ts|tsx|js|jsx)$' exclude: '\.test\.|\.spec\.|vitest\.config|vitest\.setup' pass_filenames: true - priority: 1 - - - id: ui-build - name: UI - Build - entry: pnpm run build - language: system - files: '\.(ts|tsx|js|jsx|json|css)$' - pass_filenames: false - priority: 2 + priority: 10 diff --git a/ui/.prettierignore b/ui/.prettierignore index 40b878db5b..5377d96f2d 100644 --- a/ui/.prettierignore +++ b/ui/.prettierignore @@ -1 +1,19 @@ -node_modules/ \ No newline at end of file +# Dependencies +node_modules/ + +# Build outputs +.next/ +.now/ +build/ +coverage/ +dist/ +esm/ + +# Generated files +next-env.d.ts +public/mockServiceWorker.js + +# Lockfiles +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/ui/AGENTS.md b/ui/AGENTS.md index 76f64ef251..43a26c74f9 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -1,6 +1,7 @@ # Prowler UI - AI Agent Ruleset > **Skills Reference**: For detailed patterns, use these skills: +> > - [`prowler-ui`](../skills/prowler-ui/SKILL.md) - Prowler-specific UI patterns > - [`prowler-test-ui`](../skills/prowler-test-ui/SKILL.md) - Playwright E2E testing (comprehensive) > - [`typescript`](../skills/typescript/SKILL.md) - Const types, flat interfaces @@ -18,35 +19,35 @@ When performing these actions, ALWAYS invoke the corresponding skill FIRST: -| Action | Skill | -|--------|-------| -| Add changelog entry for a PR or feature | `prowler-changelog` | -| App Router / Server Actions | `nextjs-16` | -| Building AI chat features | `ai-sdk-5` | -| Committing changes | `prowler-commit` | -| Create PR that requires changelog entry | `prowler-changelog` | -| Creating Zod schemas | `zod-4` | -| Creating a git commit | `prowler-commit` | -| 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` | -| Testing hooks or utilities | `vitest` | -| Update CHANGELOG.md in any component | `prowler-changelog` | -| Using Zustand stores | `zustand-5` | -| 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 Tailwind classes | `tailwind-4` | -| Writing Playwright E2E tests | `playwright` | -| Writing Prowler UI E2E tests | `prowler-test-ui` | -| Writing React component tests | `vitest` | -| Writing React components | `react-19` | -| Writing TypeScript types/interfaces | `typescript` | -| Writing Vitest tests | `vitest` | -| Writing unit tests for UI | `vitest` | +| Action | Skill | +| -------------------------------------------------------------- | ------------------- | +| Add changelog entry for a PR or feature | `prowler-changelog` | +| App Router / Server Actions | `nextjs-16` | +| Building AI chat features | `ai-sdk-5` | +| Committing changes | `prowler-commit` | +| Create PR that requires changelog entry | `prowler-changelog` | +| Creating Zod schemas | `zod-4` | +| Creating a git commit | `prowler-commit` | +| 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` | +| Testing hooks or utilities | `vitest` | +| Update CHANGELOG.md in any component | `prowler-changelog` | +| Using Zustand stores | `zustand-5` | +| 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 Tailwind classes | `tailwind-4` | +| Writing Playwright E2E tests | `playwright` | +| Writing Prowler UI E2E tests | `prowler-test-ui` | +| Writing React component tests | `vitest` | +| Writing React components | `react-19` | +| Writing TypeScript types/interfaces | `typescript` | +| Writing Vitest tests | `vitest` | +| Writing unit tests for UI | `vitest` | --- @@ -137,8 +138,8 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; const schema = z.object({ - email: z.email(), // Zod 4: z.email() not z.string().email() - id: z.uuid(), // Zod 4: z.uuid() not z.string().uuid() + email: z.email(), // Zod 4: z.email() not z.string().email() + id: z.uuid(), // Zod 4: z.uuid() not z.string().uuid() }); const form = useForm({ resolver: zodResolver(schema) }); @@ -163,8 +164,12 @@ const useStore = create( ```typescript export class FeaturePage extends BasePage { readonly submitBtn = this.page.getByRole("button", { name: "Submit" }); - async goto() { await super.goto("/path"); } - async submit() { await this.submitBtn.click(); } + async goto() { + await super.goto("/path"); + } + async submit() { + await this.submitBtn.click(); + } } test("action works", { tag: ["@critical", "@feature"] }, async ({ page }) => { diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 6d8e04cb63..398d5ce95f 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🔄 Changed - Trimmed unused npm dependencies [(#11115)](https://github.com/prowler-cloud/prowler/pull/11115) +- Faster, stricter pre-commit: prek lints and formats only staged UI files (husky removed), with Prettier and ESLint (`--max-warnings 40`, stale-disable detection) now covering the full UI workspace, including `public/` assets (only the auto-generated `public/mockServiceWorker.js` stays ignored) [(#11118)](https://github.com/prowler-cloud/prowler/pull/11118) - Lighthouse now accepts Prowler App Finding Groups MCP tools [(#11140)](https://github.com/prowler-cloud/prowler/pull/11140) - Attack Paths graph now uses React Flow with improved layout, interactions, export, minimap, and browser test coverage [(#10686)](https://github.com/prowler-cloud/prowler/pull/10686) - SAML ACS URL is only shown if the email domain is configured [(#11144)](https://github.com/prowler-cloud/prowler/pull/11144) diff --git a/ui/README.md b/ui/README.md index b1af73ffe3..998dc84a37 100644 --- a/ui/README.md +++ b/ui/README.md @@ -2,10 +2,12 @@ This repository hosts the UI component for Prowler, providing a user-friendly web interface to interact seamlessly with Prowler's features. - ## 🚀 Production deployment + ### Docker deployment + #### Clone the repository + ```console # HTTPS git clone https://github.com/prowler-cloud/ui.git @@ -14,16 +16,21 @@ git clone https://github.com/prowler-cloud/ui.git git clone git@github.com:prowler-cloud/ui.git ``` + #### Build the Docker image + ```bash docker build -t prowler-cloud/ui . --target prod ``` + #### Run the Docker container + ```bash docker run -p 3000:3000 prowler-cloud/ui ``` ### Local deployment + #### Clone the repository ```console @@ -48,8 +55,11 @@ pnpm start ``` ## 🧪 Development deployment + ### Docker deployment + #### Clone the repository + ```console # HTTPS git clone https://github.com/prowler-cloud/ui.git @@ -58,16 +68,21 @@ git clone https://github.com/prowler-cloud/ui.git git clone git@github.com:prowler-cloud/ui.git ``` + #### Build the Docker image + ```bash docker build -t prowler-cloud/ui . --target dev ``` + #### Run the Docker container + ```bash docker run -p 3000:3000 prowler-cloud/ui ``` ### Local deployment + #### Clone the repository ```console @@ -107,47 +122,12 @@ pnpm run dev - [Framer Motion](https://www.framer.com/motion/) - [next-themes](https://github.com/pacocoursey/next-themes) -## Git Hooks & Code Review +## Git Hooks -This project uses Git hooks to maintain code quality. When you commit changes to TypeScript/JavaScript files, the pre-commit hook can optionally validate them against our coding standards using Claude Code. - -### Enabling Code Review - -To enable automatic code review on commits, add this to your `.env` file in the project root: +The UI uses [prek](https://github.com/j178/prek) for pre-commit checks, configured in [`.pre-commit-config.yaml`](.pre-commit-config.yaml). `pnpm install` runs the postinstall script that installs hooks automatically. To re-install manually: ```bash -CODE_REVIEW_ENABLED=true -``` - -When enabled, the hook will: -- ✅ Validate your staged changes against `AGENTS.md` standards -- ✅ Check for common issues (any types, incorrect imports, styling violations, etc.) -- ✅ Block commits that don't comply with the standards -- ✅ Provide helpful feedback on how to fix issues - -### Disabling Code Review - -To disable code review (faster commits, useful for quick iterations): - -```bash -CODE_REVIEW_ENABLED=false -``` - -Or remove the variable from your `.env` file. - -### Requirements - -- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated -- `.env` file in the project root with `CODE_REVIEW_ENABLED` set - -### Troubleshooting - -If hooks aren't running after commits, verify prek is installed and hooks are set up: - -```bash -# Check prek is available -prek --version - -# Re-install hooks if needed prek install --overwrite ``` + +On each commit, prek runs Prettier and ESLint against the staged files, plus a project-wide TypeScript check and the unit tests related to the staged changes. The full Next.js build runs in CI, not on commit. diff --git a/ui/components/compliance/compliance-custom-details/cis-details.tsx b/ui/components/compliance/compliance-custom-details/cis-details.tsx index be880fd522..c46f061188 100644 --- a/ui/components/compliance/compliance-custom-details/cis-details.tsx +++ b/ui/components/compliance/compliance-custom-details/cis-details.tsx @@ -81,9 +81,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {requirement.remediation_procedure && typeof requirement.remediation_procedure === "string" && ( - {/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */} - {/* eslint-disable-next-line */} -
+
{requirement.remediation_procedure}
@@ -92,8 +90,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => { {requirement.audit_procedure && typeof requirement.audit_procedure === "string" && ( - {/* eslint-disable-next-line */} -
+
{requirement.audit_procedure}
diff --git a/ui/docs/code-review/CODE_REVIEW_SETUP.md b/ui/docs/code-review/CODE_REVIEW_SETUP.md deleted file mode 100644 index b43067f82b..0000000000 --- a/ui/docs/code-review/CODE_REVIEW_SETUP.md +++ /dev/null @@ -1,296 +0,0 @@ -# Code Review Setup - Prowler UI - -Guide to set up automatic code validation with Claude Code in the commit hook. - -## Overview - -The code review system works like this: - -1. **When you enable `CODE_REVIEW_ENABLED=true` in `.env`** - - When you `git commit`, the pre-commit hook runs - - Only validates TypeScript/JavaScript files you're committing - - Uses Claude Code to check if they comply with AGENTS.md - - If there are violations → **BLOCKS the commit** - - If everything is fine → Continues normally - -2. **When `CODE_REVIEW_ENABLED=false` (default)** - - The pre-commit hook does not run validation - - No standards validation - - Developers can commit without restrictions - -## Installation - -### 1. Ensure Claude Code is in your PATH - -```bash -# Verify that claude is available in terminal -which claude - -# If it doesn't appear, check your Claude Code CLI installation -``` - -### 2. Enable validation in `.env` - -In `/ui/.env`, find the "Code Review Configuration" section: - -```bash -#### Code Review Configuration #### -# Enable Claude Code standards validation on commit hook -# Set to 'true' to validate changes against AGENTS.md standards via Claude Code -# Set to 'false' to skip validation -CODE_REVIEW_ENABLED=false # ← Change this to 'true' -``` - -**Options:** -- `CODE_REVIEW_ENABLED=true` → Enables validation -- `CODE_REVIEW_ENABLED=false` → Disables validation (default) - -### 3. The hook is ready - -The `.husky/pre-commit` file already contains the logic. You don't need to install anything else. - -## How It Works - -### Normal Flow (with validation enabled) - -```bash -$ git commit -m "feat: add new component" - -# Pre-commit hook executes automatically -🚀 Prowler UI - Pre-Commit Hook - ℹ️ Code Review Status: true - -📋 Files to validate: - - components/new-feature.tsx - - types/new-feature.ts - -📤 Sending to Claude Code for validation... - -# Claude analyzes the files... - -=== VALIDATION REPORT === -STATUS: PASSED -All files comply with AGENTS.md standards. - -✅ VALIDATION PASSED -# Commit continues ✅ -``` - -### If There Are Violations - -```bash -$ git commit -m "feat: add new component" - -# Claude detects issues... - -=== VALIDATION REPORT === -STATUS: FAILED - -- File: components/new-feature.tsx:15 - Rule: React Imports - Issue: Using 'import * as React' instead of named imports - Expected: import { useState } from "react" - -❌ VALIDATION FAILED - -Please fix the violations before committing: - 1. Review the violations listed above - 2. Fix the code according to AGENTS.md standards - 3. Commit your changes - 4. Try again - -# Commit is BLOCKED ❌ -``` - -## What Gets Validated - -The system verifies that files comply with: - -### 1. React Imports -```typescript -// ❌ WRONG -import * as React from "react" -import React, { useState } from "react" - -// ✅ CORRECT -import { useState } from "react" -``` - -### 2. TypeScript Type Patterns -```typescript -// ❌ WRONG -type SortOption = "high-low" | "low-high" - -// ✅ CORRECT -const SORT_OPTIONS = { - HIGH_LOW: "high-low", - LOW_HIGH: "low-high", -} as const -type SortOption = typeof SORT_OPTIONS[keyof typeof SORT_OPTIONS] -``` - -### 3. Tailwind CSS -```typescript -// ❌ WRONG -className="bg-[var(--color)]" -className="text-[#ffffff]" - -// ✅ CORRECT -className="bg-card-bg text-white" -``` - -### 4. cn() Utility -```typescript -// ❌ WRONG -className={cn("flex items-center")} - -// ✅ CORRECT -className={cn("h-3 w-3", isCircle ? "rounded-full" : "rounded-sm")} -``` - -### 5. React 19 Hooks -```typescript -// ❌ WRONG -const memoized = useMemo(() => value, []) - -// ✅ CORRECT -// Don't use useMemo (React Compiler handles it) -const value = expensiveCalculation() -``` - -### 6. Zod v4 Syntax -```typescript -// ❌ WRONG -z.string().email() -z.string().nonempty() - -// ✅ CORRECT -z.email() -z.string().min(1) -``` - -### 7. File Organization -``` -// ❌ WRONG -Code used by 2+ features in feature-specific folder - -// ✅ CORRECT -Code used by 1 feature → local in that feature -Code used by 2+ features → in shared/global -``` - -### 8. Use Directives -```typescript -// ❌ WRONG -export async function updateUser() { } // Missing "use server" - -// ✅ CORRECT -"use server" -export async function updateUser() { } -``` - -## Disable Temporarily - -If you need to commit without validation temporarily: - -```bash -# Option 1: Change in .env -CODE_REVIEW_ENABLED=false -git commit - -# Option 2: Use git hook bypass -git commit --no-verify - -# Option 3: Disable the hook -chmod -x .husky/pre-commit -git commit -chmod +x .husky/pre-commit -``` - -**⚠️ Note:** `--no-verify` skips ALL hooks. - -## Troubleshooting - -### "Claude Code CLI not found" - -``` -⚠️ Claude Code CLI not found in PATH -To enable: ensure Claude Code is in PATH and CODE_REVIEW_ENABLED=true -``` - -**Solution:** -```bash -# Check where claude-code is installed -which claude-code - -# If not found, add to your ~/.zshrc: -export PATH="$HOME/.local/bin:$PATH" # or where it's installed - -# Reload the terminal -source ~/.zshrc -``` - -### "Validation inconclusive" - -If Claude Code cannot determine the status: - -``` -⚠️ Could not determine validation status -Allowing commit (validation inconclusive) -``` - -The commit is allowed automatically. If you want to be stricter, you can: - -1. Manually review files against AGENTS.md -2. Report the analysis problem to Claude - -### Build fails after validation - -``` -❌ Build failed -``` - -If validation passes but build fails: - -1. Check the build error -2. Fix it locally -3. Commit and try again - -## View the Full Report - -Reports are saved in temporary files that are deleted afterward. To see the detailed report in real-time, watch the hook output: - -```bash -git commit 2>&1 | tee commit-report.txt -``` - -This will save everything to `commit-report.txt`. - -## For the Team - -### Enable on your machine - -```bash -cd ui -# Edit .env locally and set: -CODE_REVIEW_ENABLED=true -``` - -### Recommended Flow - -1. **During development**: `CODE_REVIEW_ENABLED=false` - - Iterate faster - - Build check still runs - -2. **Before final commit**: `CODE_REVIEW_ENABLED=true` - - Verify you meet standards - - Prevent PRs rejected for violations - -3. **In CI/CD**: You could add additional validation - - (future) Server-side validation in GitHub Actions - -## Questions? - -If you have questions about the standards being validated, check: -- `AGENTS.md` - Complete architecture guide -- `CLAUDE.md` - Project-specific instructions diff --git a/ui/docs/code-review/README.md b/ui/docs/code-review/README.md deleted file mode 100644 index 9e2b1296b4..0000000000 --- a/ui/docs/code-review/README.md +++ /dev/null @@ -1,241 +0,0 @@ -# Code Review System Documentation - -Complete documentation for the Claude Code-powered commit validation system. - -## Quick Navigation - -**Want to get started in 3 steps?** -→ Read: [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md) - -**Want complete technical details?** -→ Read: [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md) - ---- - -## What This System Does - -Automatically validates code against AGENTS.md standards when you commit using Claude Code. - -``` -git commit - ↓ -(Optional) Claude Code validation - ↓ -If violations found → Commit is BLOCKED ❌ -If code complies → Commit continues ✅ -``` - -**Key Feature:** Configurable with a single variable in `.env` -- `CODE_REVIEW_ENABLED=true` → Validates (recommended before commits) -- `CODE_REVIEW_ENABLED=false` → Skip validation (default, for iteration) - ---- - -## File Guide - -| File | Purpose | Read Time | -|------|---------|-----------| -| [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md) | 3-step setup & examples | 5 min | -| [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md) | Complete technical guide | 15 min | - ---- - -## What Gets Validated - -When validation is enabled, the system checks: - -✅ **React Imports** -- Must use: `import { useState } from "react"` -- Not: `import * as React` or `import React, {` - -✅ **TypeScript Types** -- Must use: `const STATUS = {...} as const; type Status = typeof STATUS[...]` -- Not: `type Status = "a" | "b"` - -✅ **Tailwind CSS** -- Must use: `className="bg-card-bg text-white"` -- Not: `className="bg-[var(...)]"` or `className="text-[#fff]"` - -✅ **cn() Utility** -- Must use for: `cn("h-3", isActive && "bg-blue")` -- Not for: `cn("static-class")` - -✅ **React 19 Hooks** -- No: `useMemo()` / `useCallback()` without documented reason -- Use: Nothing (React Compiler handles optimization) - -✅ **Zod v4 Syntax** -- Must use: `z.email()`, `.min(1)` -- Not: `z.string().email()`, `.nonempty()` - -✅ **File Organization** -- 1 feature uses → Keep local in feature folder -- 2+ features use → Move to shared/global - -✅ **Directives** -- Server Actions must have: `"use server"` -- Client Components must have: `"use client"` - ---- - -## Installation (For Your Team) - -### Step 1: Decide if you want validation -- **Optional:** Each developer decides -- **Team policy:** Consider making it standard before commits - -### Step 2: Enable in your environment -```bash -# Edit ui/.env -CODE_REVIEW_ENABLED=true -``` - -### Step 3: Done! -Your next `git commit` will validate automatically. - ---- - -## Support - -| Question | Answer | -|----------|--------| -| How do I enable it? | Change `CODE_REVIEW_ENABLED=true` in `.env` | -| How do I disable it? | Change `CODE_REVIEW_ENABLED=false` in `.env` | -| How do I bypass? | Use `git commit --no-verify` (emergency only) | -| What if Claude Code isn't found? | Check PATH: `which claude` | -| What if hook doesn't run? | Check executable: `chmod +x .husky/pre-commit` | -| How do I test it? | Enable validation and commit code with violations to test | -| What if I don't have Claude Code? | Validation is skipped gracefully | - ---- - -## Key Features - -✅ **No Setup Required** -- Uses Claude Code already in your PATH -- No API keys needed -- Works offline (if Claude Code supports it) - -✅ **Smart Validation** -- Only checks files being committed -- Not the entire codebase -- Fast: ~10-30 seconds with validation enabled - -✅ **Flexible** -- Can be enabled/disabled per developer -- Can be disabled temporarily with `git commit --no-verify` -- Default is disabled (doesn't interrupt workflow) - -✅ **Clear Feedback** -- Shows exactly what violates standards -- Shows file:line references -- Explains how to fix each issue - -✅ **Well Documented** -- 5 different documentation files -- For different needs and levels -- Examples and troubleshooting included - ---- - -## Architecture - -``` -┌─────────────────────────────────────────┐ -│ Developer commits code │ -└────────────────┬────────────────────────┘ - ↓ - ┌─────────────────┐ - │ Pre-Commit Hook │ - │ (.husky/pre-commit) - └────────┬────────┘ - ↓ - Read CODE_REVIEW_ENABLED from .env - ↓ - ┌──────────────────────────┐ - │ If false (disabled) │ - └────────┬─────────────────┘ - ↓ - exit 0 (OK) - ↓ - Commit continues ✅ - - ┌──────────────────────────┐ - │ If true (enabled) │ - └────────┬─────────────────┘ - ↓ - Extract staged files - (git diff --cached) - ↓ - Build prompt with git diff - ↓ - Send to: claude < prompt - ↓ - Analyze against AGENTS.md - ↓ - Return: STATUS: PASSED or FAILED - ↓ - Parse with: grep "^STATUS:" - ↓ - ┌──────────────────┐ - │ PASSED detected │ - └────────┬─────────┘ - ↓ - exit 0 (OK) - ↓ - Commit continues ✅ - - ┌──────────────────┐ - │ FAILED detected │ - └────────┬─────────┘ - ↓ - Show violations - ↓ - exit 1 (FAIL) - ↓ - Commit is BLOCKED ❌ - ↓ - Developer fixes code - Developer commits again -``` - ---- - -## Getting Started - -1. **Read:** [`CODE_REVIEW_QUICK_START.md`](./CODE_REVIEW_QUICK_START.md) (5 minutes) -2. **Enable:** Set `CODE_REVIEW_ENABLED=true` in your `ui/.env` -3. **Test:** Commit some code and see validation in action -4. **For help:** See the troubleshooting section in [`CODE_REVIEW_SETUP.md`](./CODE_REVIEW_SETUP.md) - ---- - -## Implementation Details - -- **Files Modified:** 1 (`.husky/pre-commit`) -- **Files Created:** 3 (documentation) -- **Hook Size:** ~120 lines of bash -- **Dependencies:** Claude Code CLI (already available) -- **Setup Time:** 1 minute -- **Default:** Disabled (no workflow interruption) - ---- - -## Questions? - -- **How to enable?** → `CODE_REVIEW_QUICK_START.md` -- **How does it work?** → `CODE_REVIEW_SETUP.md` -- **Troubleshooting?** → See troubleshooting section in `CODE_REVIEW_SETUP.md` - ---- - -## Status - -✅ **Ready to Use** - -The system is fully implemented, documented, and tested. You can enable it immediately with a single variable change. - ---- - -**Last Updated:** November 6, 2024 -**Status:** Complete Implementation diff --git a/ui/eslint.config.mjs b/ui/eslint.config.mjs index cc90068cfa..b62359296f 100644 --- a/ui/eslint.config.mjs +++ b/ui/eslint.config.mjs @@ -41,6 +41,9 @@ export default [ // TypeScript and React files configuration { files: ["**/*.{ts,tsx,js,jsx}"], + linterOptions: { + reportUnusedDisableDirectives: "error", + }, plugins: { "@typescript-eslint": tsPlugin, "@next/next": nextPlugin, diff --git a/ui/package.json b/ui/package.json index 0b8992f0d6..df23ab653c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,12 +10,12 @@ "postinstall": "node scripts/postinstall.js", "typecheck": "tsc", "healthcheck": "pnpm run typecheck && pnpm run lint:check", - "lint:check": "eslint .", - "lint:fix": "eslint . --fix", + "lint:check": "eslint . --max-warnings 40", + "lint:fix": "eslint . --fix --max-warnings 40", "lint:knip": "knip --max-issues 494", "lint:knip:fix": "knip --fix --max-issues 494", - "format:check": "./node_modules/.bin/prettier --check ./app", - "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app", + "format:check": "./node_modules/.bin/prettier --check .", + "format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write .", "test": "vitest run", "test:watch": "vitest", "test:unit": "vitest run --project unit", diff --git a/ui/scripts/setup-git-hooks.js b/ui/scripts/setup-git-hooks.js index e46eae6e12..8e51515368 100755 --- a/ui/scripts/setup-git-hooks.js +++ b/ui/scripts/setup-git-hooks.js @@ -7,21 +7,25 @@ * If not, it runs the repository's setup script to install prek. */ -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); +const { execSync } = require("child_process"); +const fs = require("fs"); +const path = require("path"); /** * Check if prek framework is managing git hooks */ function isPrekInstalled(gitRoot) { - const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit'); + const hookPath = path.join(gitRoot, ".git", "hooks", "pre-commit"); try { if (!fs.existsSync(hookPath)) return false; - const content = fs.readFileSync(hookPath, 'utf8'); - return content.includes('prek') || content.includes('pre-commit') || content.includes('INSTALL_PYTHON'); + const content = fs.readFileSync(hookPath, "utf8"); + return ( + content.includes("prek") || + content.includes("pre-commit") || + content.includes("INSTALL_PYTHON") + ); } catch { return false; } @@ -32,9 +36,9 @@ function isPrekInstalled(gitRoot) { */ function getGitRoot() { try { - return execSync('git rev-parse --show-toplevel', { - encoding: 'utf8', - stdio: ['pipe', 'pipe', 'pipe'] + return execSync("git rev-parse --show-toplevel", { + encoding: "utf8", + stdio: ["pipe", "pipe", "pipe"], }).trim(); } catch { return null; @@ -45,51 +49,61 @@ function getGitRoot() { * Run the repository setup script */ function runSetupScript(gitRoot) { - const setupScript = path.join(gitRoot, 'scripts', 'setup-git-hooks.sh'); + const setupScript = path.join(gitRoot, "scripts", "setup-git-hooks.sh"); if (!fs.existsSync(setupScript)) { - throw new Error('Setup script not found'); + throw new Error("Setup script not found"); } execSync(`bash "${setupScript}"`, { cwd: gitRoot, - stdio: 'inherit' + stdio: "inherit", }); } // Main execution // Skip in Docker/CI environments -if (process.env.DOCKER || process.env.CI || process.env.KUBERNETES_SERVICE_HOST) { - console.log('⚠️ Running in containerized environment. Skipping git hooks setup.'); +if ( + process.env.DOCKER || + process.env.CI || + process.env.KUBERNETES_SERVICE_HOST +) { + console.log( + "⚠️ Running in containerized environment. Skipping git hooks setup.", + ); process.exit(0); } const gitRoot = getGitRoot(); if (!gitRoot) { - console.log('⚠️ Not in a git repository. Skipping git hooks setup.'); + console.log("⚠️ Not in a git repository. Skipping git hooks setup."); process.exit(0); } if (isPrekInstalled(gitRoot)) { - console.log('✅ Git hooks managed by prek framework'); - console.log(' UI hooks will be called automatically for UI files'); + console.log("✅ Git hooks managed by prek framework"); + console.log(" UI hooks will be called automatically for UI files"); process.exit(0); } // Prek not installed - set it up -console.log('⚠️ Prek hooks not installed'); -console.log('📦 Installing prek hooks...'); -console.log(''); +console.log("⚠️ Prek hooks not installed"); +console.log("📦 Installing prek hooks..."); +console.log(""); try { runSetupScript(gitRoot); - console.log(''); - console.log('✅ Prek hooks installed successfully'); + console.log(""); + console.log("✅ Prek hooks installed successfully"); } catch (error) { - console.error('❌ Failed to setup git hooks'); - console.error(' Please run manually from repo root: ./scripts/setup-git-hooks.sh'); - console.error(' Or install prek manually: https://prek.j178.dev/installation/'); + console.error("❌ Failed to setup git hooks"); + console.error( + " Please run manually from repo root: ./scripts/setup-git-hooks.sh", + ); + console.error( + " Or install prek manually: https://prek.j178.dev/installation/", + ); process.exit(1); } diff --git a/ui/scripts/update-dependency-log.js b/ui/scripts/update-dependency-log.js index ac6449e6f7..e16dba3232 100644 --- a/ui/scripts/update-dependency-log.js +++ b/ui/scripts/update-dependency-log.js @@ -1,14 +1,14 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("fs"); +const path = require("path"); function readJSON(p) { - return JSON.parse(fs.readFileSync(p, 'utf8')); + return JSON.parse(fs.readFileSync(p, "utf8")); } function getInstalledVersion(pkgName) { try { - const parts = pkgName.split('/'); - const pkgPath = path.join('node_modules', ...parts, 'package.json'); + const parts = pkgName.split("/"); + const pkgPath = path.join("node_modules", ...parts, "package.json"); const meta = readJSON(pkgPath); return meta.version; } catch (e) { @@ -25,35 +25,37 @@ function collect(sectionName, obj) { name, from: declared, to: installed || null, - strategy: 'installed', + strategy: "installed", }; }); } function main() { // If node_modules is missing, skip to avoid generating noisy diffs - if (!fs.existsSync('node_modules')) { - console.log('Skip: node_modules not found. Run npm install first.'); + if (!fs.existsSync("node_modules")) { + console.log("Skip: node_modules not found. Run npm install first."); return; } - const pkg = readJSON('package.json'); + const pkg = readJSON("package.json"); const entries = [ - ...collect('dependencies', pkg.dependencies), - ...collect('devDependencies', pkg.devDependencies), + ...collect("dependencies", pkg.dependencies), + ...collect("devDependencies", pkg.devDependencies), ]; // Stable sort by section then name entries.sort((a, b) => - a.section === b.section ? a.name.localeCompare(b.name) : a.section.localeCompare(b.section) + a.section === b.section + ? a.name.localeCompare(b.name) + : a.section.localeCompare(b.section), ); - const outPath = path.join(process.cwd(), 'dependency-log.json'); + const outPath = path.join(process.cwd(), "dependency-log.json"); // Merge with previous to preserve generatedAt when unchanged let prevMap = new Map(); if (fs.existsSync(outPath)) { try { - const prev = JSON.parse(fs.readFileSync(outPath, 'utf8')); + const prev = JSON.parse(fs.readFileSync(outPath, "utf8")); for (const e of prev) { prevMap.set(`${e.section}::${e.name}`, e); } @@ -78,10 +80,10 @@ function main() { return { ...e, from: prev.from, generatedAt: prev.generatedAt || now }; }); - const nextContent = JSON.stringify(merged, null, 2) + '\n'; + const nextContent = JSON.stringify(merged, null, 2) + "\n"; if (fs.existsSync(outPath)) { try { - const prevContent = fs.readFileSync(outPath, 'utf8'); + const prevContent = fs.readFileSync(outPath, "utf8"); if (prevContent === nextContent) { console.log(`No changes for ${outPath} (entries: ${entries.length}).`); return; diff --git a/ui/sentry/README.md b/ui/sentry/README.md index 440ff8553b..f33f9f2a8f 100644 --- a/ui/sentry/README.md +++ b/ui/sentry/README.md @@ -45,8 +45,9 @@ NEXT_PUBLIC_SENTRY_ENVIRONMENT=development ## Ignored Errors The following errors are intentionally ignored as they are expected behavior: + - `NEXT_REDIRECT` - Next.js redirect mechanism - `NEXT_NOT_FOUND` - Next.js 404 handling - `401` - Unauthorized (expected when token expires) - `403` - Forbidden (expected for permission checks) -- `404` - Not Found (expected for missing resources) \ No newline at end of file +- `404` - Not Found (expected for missing resources) diff --git a/ui/styles/globals.css b/ui/styles/globals.css index 3eb7ffa6e0..1d7587f748 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -28,7 +28,7 @@ --bg-input-primary: var(--color-white); --border-input-primary: var(--color-slate-400); --border-input-primary-press: var(--color-slate-700); - --border-input-primary-pressed: #A7F3D0; + --border-input-primary-pressed: #a7f3d0; --border-input-primary-fill: var(--color-slate-500); /* Text Colors */ @@ -83,7 +83,7 @@ 0 0 10px var(--bg-button-primary), 0 0 5px var(--bg-button-primary); /* Lighthouse AI */ - --gradient-lighthouse: linear-gradient(96deg, #2EE59B 3.55%, #62DFF0 98.85%); + --gradient-lighthouse: linear-gradient(96deg, #2ee59b 3.55%, #62dff0 98.85%); } /* ===== DARK THEME ===== */ @@ -106,7 +106,7 @@ --bg-input-primary: var(--color-neutral-900); --border-input-primary: var(--color-neutral-800); --border-input-primary-press: var(--color-neutral-800); - --border-input-primary-pressed: #A7F3D0; + --border-input-primary-pressed: #a7f3d0; --border-input-primary-fill: var(--color-neutral-300); /* Text Colors */ diff --git a/ui/tests/auth/auth-middleware.spec.ts b/ui/tests/auth/auth-middleware.spec.ts index b56a2b63ca..a3e77c7eb4 100644 --- a/ui/tests/auth/auth-middleware.spec.ts +++ b/ui/tests/auth/auth-middleware.spec.ts @@ -85,18 +85,15 @@ test.describe("Middleware Error Handling", () => { await context.clearCookies(); const token = "test-token-regression"; - const response = await page.goto( - `/sign-up?invitation_token=${token}`, - { waitUntil: "commit" }, - ); + const response = await page.goto(`/sign-up?invitation_token=${token}`, { + waitUntil: "commit", + }); // The middleware must not rewrite the URL any more. Assert the final // URL stayed on /sign-up with the token intact, and that the sign-up // form actually rendered (guards against "URL stayed but page broke"). expect(response?.status()).toBe(200); - await expect(page).toHaveURL( - `/sign-up?invitation_token=${token}`, - ); + await expect(page).toHaveURL(`/sign-up?invitation_token=${token}`); await signUpPage.verifyPageLoaded(); }, ); diff --git a/ui/tests/auth/auth.md b/ui/tests/auth/auth.md index bc689502a3..91b34110d5 100644 --- a/ui/tests/auth/auth.md +++ b/ui/tests/auth/auth.md @@ -10,16 +10,19 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @middleware **Description/Objective:** Verify public routes are accessible without authentication. **Preconditions:** + - Application is running. - No active session (cookies cleared). ### Flow Steps: + 1. Clear all cookies. 2. Navigate to /sign-in. 3. Verify page loads. @@ -27,6 +30,7 @@ 5. Verify page loads. ### Expected Result: + - Public routes are accessible without authentication. --- @@ -36,15 +40,18 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @middleware **Description/Objective:** Verify protected routes remain protected after session invalidation. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Log in with valid credentials. 2. Navigate to a protected route. 3. Invalidate session (replace cookie with invalid token). @@ -52,6 +59,7 @@ 5. Verify redirect to sign-in. ### Expected Result: + - Invalid session results in redirect to sign-in. --- @@ -61,20 +69,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @session **Description/Objective:** Verify that RefreshAccessTokenError displays appropriate toast message. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to /sign-in with error=RefreshAccessTokenError query parameter. 2. Check for toast notification. 3. Verify form elements are still visible. ### Expected Result: + - Toast shows "Session Expired" message with "Please sign in again". - Sign-in form is displayed and functional. @@ -85,20 +97,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @session **Description/Objective:** Verify that MissingRefreshToken error displays appropriate toast message. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to /sign-in with error=MissingRefreshToken query parameter. 2. Check for toast notification. 3. Verify email input is visible. ### Expected Result: + - Toast shows "Session Error" message. - Sign-in form is displayed. @@ -109,19 +125,23 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @session **Description/Objective:** Verify that unknown error types display a generic authentication error message. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to /sign-in with error=UnknownError query parameter. 2. Check for toast notification. ### Expected Result: + - Toast shows "Authentication Error" message with "Please sign in again". --- @@ -131,16 +151,19 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @session **Description/Objective:** Verify that callbackUrl is preserved when redirecting to sign-in after session expiry. **Preconditions:** + - Application is running. - Valid test user credentials. ### Flow Steps: + 1. Log in with valid credentials. 2. Navigate to a protected route (/scans). 3. Navigate to a safe public page (/sign-in). @@ -149,6 +172,7 @@ 6. Verify redirect to sign-in includes callbackUrl parameter. ### Expected Result: + - URL contains callbackUrl=/providers parameter. - User can sign in and be redirected back to the original destination. @@ -159,16 +183,19 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @token **Description/Objective:** Verify that session is maintained after page reload (token refresh). **Preconditions:** + - Application is running. - Valid test user credentials. ### Flow Steps: + 1. Log in with valid credentials. 2. Verify home page is loaded. 3. Capture initial session data. @@ -176,6 +203,7 @@ 5. Verify session is still valid with same user data. ### Expected Result: + - Session persists after reload. - User email, userId, and tenantId remain the same. @@ -186,22 +214,26 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @token **Description/Objective:** Verify that user permissions are preserved after token refresh. **Preconditions:** + - Application is running. - Valid test user credentials. ### Flow Steps: + 1. Log in with valid credentials. 2. Capture initial session with permissions. 3. Reload the page. 4. Verify permissions match initial session. ### Expected Result: + - User permissions are identical before and after refresh. - User profile data (email, name, companyName) is preserved. @@ -212,21 +244,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @auth, @token **Description/Objective:** Verify that session is cleared when cookies are removed. **Preconditions:** + - Application is running. - Valid test user credentials. ### Flow Steps: + 1. Log in with valid credentials. 2. Verify session is valid. 3. Clear all cookies. 4. Check session status. ### Expected Result: + - Session returns null after cookies are cleared. - User is effectively logged out. diff --git a/ui/tests/base-page.ts b/ui/tests/base-page.ts index 2afadd4f2d..0f55c3deea 100644 --- a/ui/tests/base-page.ts +++ b/ui/tests/base-page.ts @@ -77,11 +77,17 @@ export abstract class BasePage { await expect(element).not.toBeVisible(); } - async verifyElementText(element: Locator, expectedText: string): Promise { + async verifyElementText( + element: Locator, + expectedText: string, + ): Promise { await expect(element).toHaveText(expectedText); } - async verifyElementContainsText(element: Locator, expectedText: string): Promise { + async verifyElementContainsText( + element: Locator, + expectedText: string, + ): Promise { await expect(element).toContainText(expectedText); } @@ -93,7 +99,9 @@ export abstract class BasePage { } } - async verifyAriaLabels(elements: { locator: Locator; expectedLabel: string }[]): Promise { + async verifyAriaLabels( + elements: { locator: Locator; expectedLabel: string }[], + ): Promise { for (const { locator, expectedLabel } of elements) { await expect(locator).toHaveAttribute("aria-label", expectedLabel); } @@ -101,7 +109,7 @@ export abstract class BasePage { // Common utility methods async getElementText(element: Locator): Promise { - return await element.textContent() || ""; + return (await element.textContent()) || ""; } async getElementValue(element: Locator): Promise { @@ -118,7 +126,9 @@ export abstract class BasePage { // Common error handling methods async getFormErrors(): Promise { - const errorElements = await this.page.locator('[role="alert"], .error-message, [data-testid="error"]').all(); + const errorElements = await this.page + .locator('[role="alert"], .error-message, [data-testid="error"]') + .all(); const errors: string[] = []; for (const element of errorElements) { @@ -137,29 +147,33 @@ export abstract class BasePage { } // Common wait methods - async waitForElement(element: Locator, timeout: number = 5000): Promise { - + async waitForElement( + element: Locator, + timeout: number = 5000, + ): Promise { await element.waitFor({ timeout }); } - async waitForElementToDisappear(element: Locator, timeout: number = 5000): Promise { - + async waitForElementToDisappear( + element: Locator, + timeout: number = 5000, + ): Promise { await element.waitFor({ state: "hidden", timeout }); } - async waitForUrl(expectedUrl: string | RegExp, timeout: number = 5000): Promise { - + async waitForUrl( + expectedUrl: string | RegExp, + timeout: number = 5000, + ): Promise { await this.page.waitForURL(expectedUrl, { timeout }); } // Common screenshot methods async takeScreenshot(name: string): Promise { - await this.page.screenshot({ path: `screenshots/${name}.png` }); } async takeElementScreenshot(element: Locator, name: string): Promise { - await element.screenshot({ path: `screenshots/${name}.png` }); } } diff --git a/ui/tests/helpers.ts b/ui/tests/helpers.ts index d4f01f8217..2bbbdeaeec 100644 --- a/ui/tests/helpers.ts +++ b/ui/tests/helpers.ts @@ -1,5 +1,10 @@ import { Locator, Page, expect, request } from "@playwright/test"; -import { AWSProviderCredential, AWSProviderData, AWS_CREDENTIAL_OPTIONS, ProvidersPage } from "./providers/providers-page"; +import { + AWSProviderCredential, + AWSProviderData, + AWS_CREDENTIAL_OPTIONS, + ProvidersPage, +} from "./providers/providers-page"; import { ScansPage } from "./scans/scans-page"; export const ERROR_MESSAGES = { @@ -70,7 +75,6 @@ export async function verifySessionValid(page: Page) { return session; } - export async function addAWSProvider( page: Page, accountId: string, @@ -128,7 +132,10 @@ export async function addAWSProvider( await scansPage.verifyPageLoaded(); } -export async function deleteProviderIfExists(page: ProvidersPage, providerUID: string): Promise { +export async function deleteProviderIfExists( + page: ProvidersPage, + providerUID: string, +): Promise { // Delete the provider if it exists // Navigate to providers page @@ -178,11 +185,7 @@ export async function deleteProviderIfExists(page: ProvidersPage, providerUID: s } // Find and click the action button (last cell = actions column) - const actionButton = targetRow - .locator("td") - .last() - .locator("button") - .first(); + const actionButton = targetRow.locator("td").last().locator("button").first(); // Ensure the button is in view before clicking (handles horizontal scroll) await actionButton.scrollIntoViewIfNeeded(); diff --git a/ui/tests/home/home-page.ts b/ui/tests/home/home-page.ts index 571ace684f..e61be49b76 100644 --- a/ui/tests/home/home-page.ts +++ b/ui/tests/home/home-page.ts @@ -2,7 +2,6 @@ import { Page, Locator, expect } from "@playwright/test"; import { BasePage } from "../base-page"; export class HomePage extends BasePage { - // Main content elements readonly mainContent: Locator; readonly breadcrumbs: Locator; @@ -26,7 +25,10 @@ export class HomePage extends BasePage { // Main content elements this.mainContent = page.locator("main"); this.breadcrumbs = page.getByRole("navigation", { name: "Breadcrumbs" }); - this.overviewHeading = page.getByRole("heading", { name: "Overview", exact: true }); + this.overviewHeading = page.getByRole("heading", { + name: "Overview", + exact: true, + }); // Navigation elements this.navigationMenu = page.locator("nav"); diff --git a/ui/tests/invitations/invitations.md b/ui/tests/invitations/invitations.md index 9331d0e11c..51d78356df 100644 --- a/ui/tests/invitations/invitations.md +++ b/ui/tests/invitations/invitations.md @@ -63,4 +63,4 @@ - Test uses a fresh browser context for the invitee to avoid admin session leakage - Email should be unique per run (the test uses a random suffix) - Ensure `E2E_NEW_USER_PASSWORD` and `E2E_ORGANIZATION_ID` are set before execution -- The role `e2e_admin` must be available in the environment \ No newline at end of file +- The role `e2e_admin` must be available in the environment diff --git a/ui/tests/providers/providers-page.ts b/ui/tests/providers/providers-page.ts index a5833768b3..5b73269062 100644 --- a/ui/tests/providers/providers-page.ts +++ b/ui/tests/providers/providers-page.ts @@ -944,9 +944,7 @@ export class ProvidersPage extends BasePage { const secretAccessKey = credentials.secretAccessKey || process.env.E2E_AWS_PROVIDER_SECRET_KEY; - const shouldFillStaticKeys = Boolean( - accessKeyId || secretAccessKey, - ); + const shouldFillStaticKeys = Boolean(accessKeyId || secretAccessKey); if (shouldFillStaticKeys) { const accessKeyIsVisible = await accessKeyInputInWizard .isVisible() diff --git a/ui/tests/scans/scans-page.ts b/ui/tests/scans/scans-page.ts index a161ea89ae..f3aa374ff1 100644 --- a/ui/tests/scans/scans-page.ts +++ b/ui/tests/scans/scans-page.ts @@ -3,29 +3,35 @@ import { BasePage } from "../base-page"; // Scan page export class ScansPage extends BasePage { - // Main content elements readonly scanTable: Locator; - // Scan provider selection elements - readonly scanProviderSelect: Locator; - readonly scanAliasInput: Locator; - readonly startNowButton: Locator; - - // Scan state elements - readonly successToast: Locator; + // Scan provider selection elements + readonly scanProviderSelect: Locator; + readonly scanAliasInput: Locator; + readonly startNowButton: Locator; + // Scan state elements + readonly successToast: Locator; constructor(page: Page) { super(page); // Scan provider selection elements - this.scanProviderSelect = page.getByRole('combobox').filter({ hasText: 'Choose a provider' }) - this.scanAliasInput = page.getByRole("textbox", { name: "Scan label (optional)" }); - this.startNowButton = page.getByRole("button", { name: /Start now|Start scan now/i }); + this.scanProviderSelect = page + .getByRole("combobox") + .filter({ hasText: "Choose a provider" }); + this.scanAliasInput = page.getByRole("textbox", { + name: "Scan label (optional)", + }); + this.startNowButton = page.getByRole("button", { + name: /Start now|Start scan now/i, + }); // Scan state elements - this.successToast = page.getByRole("alert", { name: /The scan was launched successfully\.?/i }); + this.successToast = page.getByRole("alert", { + name: /The scan was launched successfully\.?/i, + }); // Main content elements this.scanTable = page.locator("table"); @@ -70,7 +76,9 @@ export class ScansPage extends BasePage { // Verify the scan was launched // Verify the success toast is visible - await this.successToast.waitFor({ state: "visible", timeout: 5000 }).catch(() => {}); + await this.successToast + .waitFor({ state: "visible", timeout: 5000 }) + .catch(() => {}); // Wait for the scans table to be visible await expect(this.scanTable).toBeVisible(); @@ -86,7 +94,6 @@ export class ScansPage extends BasePage { // Basic state/assertion hint: queued/available/executing (non-blocking if not present) await rowWithAlias.textContent().then((text) => { - if (!text) return; const hasExpectedState = /executing|available|queued/i.test(text); @@ -98,7 +105,6 @@ export class ScansPage extends BasePage { }); } - async verifyScheduledScanStatus(accountId: string): Promise { // Verifies that: // 1. The provider exists in the table (by account ID/UID) @@ -133,5 +139,4 @@ export class ScansPage extends BasePage { ignoreCase: true, }); } - } diff --git a/ui/tests/scans/scans.md b/ui/tests/scans/scans.md index cca43e487e..7b8735cc1f 100644 --- a/ui/tests/scans/scans.md +++ b/ui/tests/scans/scans.md @@ -51,5 +51,3 @@ - The table may take a short time to reflect the new scan; assertions look for a row containing the alias. - Provider cleanup performed before each test to ensure clean state - Tests should run serially to avoid state conflicts. - - diff --git a/ui/tests/scans/scans.spec.ts b/ui/tests/scans/scans.spec.ts index 525b55ebd8..a5cb020e5a 100644 --- a/ui/tests/scans/scans.spec.ts +++ b/ui/tests/scans/scans.spec.ts @@ -40,7 +40,6 @@ test.describe("Scans", () => { tag: ["@e2e", "@scans", "@critical", "@serial", "@SCAN-E2E-001"], }, async ({ page }) => { - const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID; if (!accountId) { @@ -63,8 +62,6 @@ test.describe("Scans", () => { // Verify the scan was launched await scansPage.verifyScanLaunched("E2E Test Scan - On Demand"); - - }, ); }); diff --git a/ui/tests/setups/admin.auth.setup.ts b/ui/tests/setups/admin.auth.setup.ts index 86094cc1cf..c2d62117f3 100644 --- a/ui/tests/setups/admin.auth.setup.ts +++ b/ui/tests/setups/admin.auth.setup.ts @@ -1,17 +1,21 @@ -import { test as authAdminSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authAdminSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const adminUserFile = 'playwright/.auth/admin_user.json'; - -authAdminSetup('authenticate as admin e2e user', async ({ page }) => { +const adminUserFile = "playwright/.auth/admin_user.json"; +authAdminSetup("authenticate as admin e2e user", async ({ page }) => { const adminEmail = process.env.E2E_ADMIN_USER; const adminPassword = process.env.E2E_ADMIN_PASSWORD; if (!adminEmail || !adminPassword) { - throw new Error('E2E_ADMIN_USER and E2E_ADMIN_PASSWORD environment variables are required'); + throw new Error( + "E2E_ADMIN_USER and E2E_ADMIN_PASSWORD environment variables are required", + ); } const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: adminEmail, password: adminPassword }, adminUserFile); -}); \ No newline at end of file + await signInPage.authenticateAndSaveState( + { email: adminEmail, password: adminPassword }, + adminUserFile, + ); +}); diff --git a/ui/tests/setups/invite-and-manage-users.auth.setup.ts b/ui/tests/setups/invite-and-manage-users.auth.setup.ts index c9dc6cf273..f64271b10b 100644 --- a/ui/tests/setups/invite-and-manage-users.auth.setup.ts +++ b/ui/tests/setups/invite-and-manage-users.auth.setup.ts @@ -1,16 +1,30 @@ -import { test as authInviteAndManageUsersSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authInviteAndManageUsersSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const inviteAndManageUsersUserFile = 'playwright/.auth/invite_and_manage_users_user.json'; +const inviteAndManageUsersUserFile = + "playwright/.auth/invite_and_manage_users_user.json"; -authInviteAndManageUsersSetup('authenticate as invite and manage users e2e user', async ({ page }) => { - const inviteAndManageUsersEmail = process.env.E2E_INVITE_AND_MANAGE_USERS_USER; - const inviteAndManageUsersPassword = process.env.E2E_INVITE_AND_MANAGE_USERS_PASSWORD; - - if (!inviteAndManageUsersEmail || !inviteAndManageUsersPassword) { - throw new Error('E2E_INVITE_AND_MANAGE_USERS_USER and E2E_INVITE_AND_MANAGE_USERS_PASSWORD environment variables are required'); - } +authInviteAndManageUsersSetup( + "authenticate as invite and manage users e2e user", + async ({ page }) => { + const inviteAndManageUsersEmail = + process.env.E2E_INVITE_AND_MANAGE_USERS_USER; + const inviteAndManageUsersPassword = + process.env.E2E_INVITE_AND_MANAGE_USERS_PASSWORD; - const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: inviteAndManageUsersEmail, password: inviteAndManageUsersPassword }, inviteAndManageUsersUserFile); -}); + if (!inviteAndManageUsersEmail || !inviteAndManageUsersPassword) { + throw new Error( + "E2E_INVITE_AND_MANAGE_USERS_USER and E2E_INVITE_AND_MANAGE_USERS_PASSWORD environment variables are required", + ); + } + + const signInPage = new SignInPage(page); + await signInPage.authenticateAndSaveState( + { + email: inviteAndManageUsersEmail, + password: inviteAndManageUsersPassword, + }, + inviteAndManageUsersUserFile, + ); + }, +); diff --git a/ui/tests/setups/manage-account.auth.setup.ts b/ui/tests/setups/manage-account.auth.setup.ts index 18e394b753..4180640eba 100644 --- a/ui/tests/setups/manage-account.auth.setup.ts +++ b/ui/tests/setups/manage-account.auth.setup.ts @@ -1,16 +1,24 @@ -import { test as authManageAccountSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authManageAccountSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const manageAccountUserFile = 'playwright/.auth/manage_account_user.json'; +const manageAccountUserFile = "playwright/.auth/manage_account_user.json"; -authManageAccountSetup('authenticate as manage account e2e user', async ({ page }) => { - const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER; - const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD; - - if (!accountEmail || !accountPassword) { - throw new Error('E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required'); - } +authManageAccountSetup( + "authenticate as manage account e2e user", + async ({ page }) => { + const accountEmail = process.env.E2E_MANAGE_ACCOUNT_USER; + const accountPassword = process.env.E2E_MANAGE_ACCOUNT_PASSWORD; - const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: accountEmail, password: accountPassword }, manageAccountUserFile); -}); + if (!accountEmail || !accountPassword) { + throw new Error( + "E2E_MANAGE_ACCOUNT_USER and E2E_MANAGE_ACCOUNT_PASSWORD environment variables are required", + ); + } + + const signInPage = new SignInPage(page); + await signInPage.authenticateAndSaveState( + { email: accountEmail, password: accountPassword }, + manageAccountUserFile, + ); + }, +); diff --git a/ui/tests/setups/manage-cloud-providers.auth.setup.ts b/ui/tests/setups/manage-cloud-providers.auth.setup.ts index 96578f276c..98353b9125 100644 --- a/ui/tests/setups/manage-cloud-providers.auth.setup.ts +++ b/ui/tests/setups/manage-cloud-providers.auth.setup.ts @@ -1,16 +1,26 @@ -import { test as authManageCloudProvidersSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authManageCloudProvidersSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const manageCloudProvidersUserFile = 'playwright/.auth/manage_cloud_providers_user.json'; +const manageCloudProvidersUserFile = + "playwright/.auth/manage_cloud_providers_user.json"; -authManageCloudProvidersSetup('authenticate as manage providers e2e user', async ({ page }) => { - const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER; - const cloudProvidersPassword = process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD; - - if (!cloudProvidersEmail || !cloudProvidersPassword) { - throw new Error('E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required'); - } +authManageCloudProvidersSetup( + "authenticate as manage providers e2e user", + async ({ page }) => { + const cloudProvidersEmail = process.env.E2E_MANAGE_CLOUD_PROVIDERS_USER; + const cloudProvidersPassword = + process.env.E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD; - const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: cloudProvidersEmail, password: cloudProvidersPassword }, manageCloudProvidersUserFile); -}); + if (!cloudProvidersEmail || !cloudProvidersPassword) { + throw new Error( + "E2E_MANAGE_CLOUD_PROVIDERS_USER and E2E_MANAGE_CLOUD_PROVIDERS_PASSWORD environment variables are required", + ); + } + + const signInPage = new SignInPage(page); + await signInPage.authenticateAndSaveState( + { email: cloudProvidersEmail, password: cloudProvidersPassword }, + manageCloudProvidersUserFile, + ); + }, +); diff --git a/ui/tests/setups/manage-integrations.auth.setup.ts b/ui/tests/setups/manage-integrations.auth.setup.ts index ae53df5458..a6ada3d7f9 100644 --- a/ui/tests/setups/manage-integrations.auth.setup.ts +++ b/ui/tests/setups/manage-integrations.auth.setup.ts @@ -1,16 +1,25 @@ -import { test as authManageIntegrationsSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authManageIntegrationsSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const manageIntegrationsUserFile = 'playwright/.auth/manage_integrations_user.json'; +const manageIntegrationsUserFile = + "playwright/.auth/manage_integrations_user.json"; -authManageIntegrationsSetup('authenticate as integrations e2e user', async ({ page }) => { - const integrationsEmail = process.env.E2E_MANAGE_INTEGRATIONS_USER; - const integrationsPassword = process.env.E2E_MANAGE_INTEGRATIONS_PASSWORD; - - if (!integrationsEmail || !integrationsPassword) { - throw new Error('E2E_MANAGE_INTEGRATIONS_USER and E2E_MANAGE_INTEGRATIONS_PASSWORD environment variables are required'); - } +authManageIntegrationsSetup( + "authenticate as integrations e2e user", + async ({ page }) => { + const integrationsEmail = process.env.E2E_MANAGE_INTEGRATIONS_USER; + const integrationsPassword = process.env.E2E_MANAGE_INTEGRATIONS_PASSWORD; - const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: integrationsEmail, password: integrationsPassword }, manageIntegrationsUserFile); -}); + if (!integrationsEmail || !integrationsPassword) { + throw new Error( + "E2E_MANAGE_INTEGRATIONS_USER and E2E_MANAGE_INTEGRATIONS_PASSWORD environment variables are required", + ); + } + + const signInPage = new SignInPage(page); + await signInPage.authenticateAndSaveState( + { email: integrationsEmail, password: integrationsPassword }, + manageIntegrationsUserFile, + ); + }, +); diff --git a/ui/tests/setups/manage-scans.auth.setup.ts b/ui/tests/setups/manage-scans.auth.setup.ts index 84d41a657d..e31c213d22 100644 --- a/ui/tests/setups/manage-scans.auth.setup.ts +++ b/ui/tests/setups/manage-scans.auth.setup.ts @@ -1,16 +1,21 @@ -import { test as authManageScansSetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authManageScansSetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const manageScansUserFile = 'playwright/.auth/manage_scans_user.json'; +const manageScansUserFile = "playwright/.auth/manage_scans_user.json"; -authManageScansSetup('authenticate as scans e2e user', async ({ page }) => { +authManageScansSetup("authenticate as scans e2e user", async ({ page }) => { const scansEmail = process.env.E2E_MANAGE_SCANS_USER; const scansPassword = process.env.E2E_MANAGE_SCANS_PASSWORD; - + if (!scansEmail || !scansPassword) { - throw new Error('E2E_MANAGE_SCANS_USER and E2E_MANAGE_SCANS_PASSWORD environment variables are required'); + throw new Error( + "E2E_MANAGE_SCANS_USER and E2E_MANAGE_SCANS_PASSWORD environment variables are required", + ); } const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: scansEmail, password: scansPassword }, manageScansUserFile); + await signInPage.authenticateAndSaveState( + { email: scansEmail, password: scansPassword }, + manageScansUserFile, + ); }); diff --git a/ui/tests/setups/unlimited-visibility.auth.setup.ts b/ui/tests/setups/unlimited-visibility.auth.setup.ts index 0c5d0fd1ce..1402e7457c 100644 --- a/ui/tests/setups/unlimited-visibility.auth.setup.ts +++ b/ui/tests/setups/unlimited-visibility.auth.setup.ts @@ -1,16 +1,29 @@ -import { test as authUnlimitedVisibilitySetup } from '@playwright/test'; -import { SignInPage } from '../sign-in-base/sign-in-base-page'; +import { test as authUnlimitedVisibilitySetup } from "@playwright/test"; +import { SignInPage } from "../sign-in-base/sign-in-base-page"; -const unlimitedVisibilityUserFile = 'playwright/.auth/unlimited_visibility_user.json'; +const unlimitedVisibilityUserFile = + "playwright/.auth/unlimited_visibility_user.json"; -authUnlimitedVisibilitySetup('authenticate as unlimited visibility e2e user', async ({ page }) => { - const unlimitedVisibilityEmail = process.env.E2E_UNLIMITED_VISIBILITY_USER; - const unlimitedVisibilityPassword = process.env.E2E_UNLIMITED_VISIBILITY_PASSWORD; - - if (!unlimitedVisibilityEmail || !unlimitedVisibilityPassword) { - throw new Error('E2E_UNLIMITED_VISIBILITY_USER and E2E_UNLIMITED_VISIBILITY_PASSWORD environment variables are required'); - } +authUnlimitedVisibilitySetup( + "authenticate as unlimited visibility e2e user", + async ({ page }) => { + const unlimitedVisibilityEmail = process.env.E2E_UNLIMITED_VISIBILITY_USER; + const unlimitedVisibilityPassword = + process.env.E2E_UNLIMITED_VISIBILITY_PASSWORD; - const signInPage = new SignInPage(page); - await signInPage.authenticateAndSaveState({ email: unlimitedVisibilityEmail, password: unlimitedVisibilityPassword }, unlimitedVisibilityUserFile); -}); + if (!unlimitedVisibilityEmail || !unlimitedVisibilityPassword) { + throw new Error( + "E2E_UNLIMITED_VISIBILITY_USER and E2E_UNLIMITED_VISIBILITY_PASSWORD environment variables are required", + ); + } + + const signInPage = new SignInPage(page); + await signInPage.authenticateAndSaveState( + { + email: unlimitedVisibilityEmail, + password: unlimitedVisibilityPassword, + }, + unlimitedVisibilityUserFile, + ); + }, +); diff --git a/ui/tests/sign-in-base/sign-in-base.md b/ui/tests/sign-in-base/sign-in-base.md index 8298b375fb..5054fc998e 100644 --- a/ui/tests/sign-in-base/sign-in-base.md +++ b/ui/tests/sign-in-base/sign-in-base.md @@ -10,15 +10,18 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify that all login form elements are displayed correctly. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Verify page is loaded. 3. Verify form elements (email, password, login button). @@ -26,6 +29,7 @@ 5. Verify navigation links. ### Expected Result: + - All form elements are visible and properly labeled. --- @@ -35,22 +39,26 @@ **Priority:** `critical` **Tags:** + - type: @e2e, @critical - feature: @sign-in-base **Description/Objective:** Verify that a user can successfully log in with valid credentials. **Preconditions:** + - Application is running. - Valid test user credentials are configured via `ADMIN_USER` and `ADMIN_PASSWORD` environment variables. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Enter valid email and password. 3. Click the login button. 4. Verify successful redirect to home page. ### Expected Result: + - User is authenticated and redirected to the home page. --- @@ -60,21 +68,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify that an error message is shown when invalid credentials are provided. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Enter invalid email and password. 3. Click the login button. 4. Verify error message is displayed. ### Expected Result: + - Error message "Invalid email or password" is displayed. - User remains on the sign-in page. @@ -85,20 +97,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify form validation when submitting an empty form. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Click the login button without filling any fields. 3. Verify validation errors are displayed. ### Expected Result: + - Form validation errors are shown. - User remains on the sign-in page. @@ -109,21 +125,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify that invalid email formats are rejected. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Enter an invalid email format. 3. Submit the form. 4. Verify validation error is displayed. ### Expected Result: + - Email format validation error is shown. --- @@ -133,21 +153,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify that password is required when email is provided. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Fill only the email field. 3. Submit the form. 4. Verify password required error is displayed. ### Expected Result: + - "Password is required" error is shown. --- @@ -157,15 +181,18 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify SAML SSO mode can be toggled on and off. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Click "Continue with SAML SSO" button. 3. Verify SAML mode is active (password field hidden). @@ -173,6 +200,7 @@ 5. Verify normal mode is restored. ### Expected Result: + - SAML mode toggles correctly. - Password field visibility changes accordingly. @@ -183,21 +211,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify loading state is shown during form submission. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Fill valid credentials. 3. Submit the form. 4. Verify loading state on button. ### Expected Result: + - Login button shows loading state (disabled with aria-disabled). --- @@ -207,21 +239,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify SAML authentication flow initiation. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Toggle SAML mode. 3. Enter SAML email. 4. Submit the form. ### Expected Result: + - SAML flow is initiated (would redirect to IdP in real scenario). --- @@ -231,22 +267,26 @@ **Priority:** `critical` **Tags:** + - type: @e2e, @critical - feature: @sign-in-base **Description/Objective:** Verify that user session persists after page refresh. **Preconditions:** + - Application is running. - Valid test user credentials. ### Flow Steps: + 1. Log in with valid credentials. 2. Verify successful login. 3. Refresh the page. 4. Verify user is still logged in. ### Expected Result: + - Session persists after refresh. - User remains on the authenticated page. @@ -257,20 +297,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify unauthenticated users are redirected to login when accessing protected routes. **Preconditions:** + - Application is running. - No active session. ### Flow Steps: + 1. Navigate directly to a protected route (e.g., /providers). 2. Verify redirect to sign-in page. ### Expected Result: + - User is redirected to /sign-in. --- @@ -280,16 +324,19 @@ **Priority:** `critical` **Tags:** + - type: @e2e, @critical - feature: @sign-in-base **Description/Objective:** Verify user can log out successfully. **Preconditions:** + - Application is running. - User is logged in. ### Flow Steps: + 1. Log in with valid credentials. 2. Click logout/sign out. 3. Verify redirect to sign-in page. @@ -297,6 +344,7 @@ 5. Verify redirect to sign-in. ### Expected Result: + - User is logged out. - Session is invalidated. - Protected routes are no longer accessible. @@ -308,15 +356,18 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify session isolation between browser contexts. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Create authenticated context and log in. 2. Verify session exists. 3. Create new unauthenticated context. @@ -324,6 +375,7 @@ 5. Verify new context is redirected to sign-in. ### Expected Result: + - Sessions are isolated between contexts. - Unauthenticated context cannot access protected routes. @@ -334,20 +386,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify navigation from sign-in to sign-up page. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Click the "Sign up" link. 3. Verify redirect to sign-up page. ### Expected Result: + - User is navigated to /sign-up. --- @@ -357,20 +413,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify navigation from sign-up back to sign-in page. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign up page. 2. Click the login link. 3. Verify redirect to sign-in page. ### Expected Result: + - User is navigated to /sign-in. --- @@ -380,21 +440,25 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base **Description/Objective:** Verify browser back button navigation works correctly. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Navigate to the Sign up page. 3. Click browser back button. 4. Verify return to sign-in page. ### Expected Result: + - Browser history navigation works correctly. --- @@ -404,20 +468,24 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base, @accessibility **Description/Objective:** Verify form is navigable with keyboard. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Use Tab key to navigate through form elements. 3. Verify focus moves correctly through elements. ### Expected Result: + - All interactive elements are reachable via keyboard. - Focus order is logical. @@ -428,19 +496,23 @@ **Priority:** `normal` **Tags:** + - type: @e2e - feature: @sign-in-base, @accessibility **Description/Objective:** Verify form elements have proper ARIA labels. **Preconditions:** + - Application is running. ### Flow Steps: + 1. Navigate to the Sign in page. 2. Verify ARIA labels on form elements. ### Expected Result: + - Email input has proper label. - Password input has proper label. - Login button has proper label. diff --git a/ui/tests/sign-up/sign-up-page.ts b/ui/tests/sign-up/sign-up-page.ts index 0afaccb6bf..16ab740f48 100644 --- a/ui/tests/sign-up/sign-up-page.ts +++ b/ui/tests/sign-up/sign-up-page.ts @@ -12,7 +12,6 @@ export interface SignUpData { } export class SignUpPage extends BasePage { - // Form inputs readonly nameInput: Locator; readonly companyInput: Locator; @@ -39,7 +38,9 @@ export class SignUpPage extends BasePage { this.submitButton = page.getByRole("button", { name: "Sign up" }); this.loginLink = page.getByRole("link", { name: "Log in" }); - this.termsCheckbox = page.getByRole("checkbox", { name: /I agree with the/i }); + this.termsCheckbox = page.getByRole("checkbox", { + name: /I agree with the/i, + }); } async goto(): Promise { @@ -50,7 +51,7 @@ export class SignUpPage extends BasePage { async gotoInvite(shareUrl: string): Promise { // Navigate to the share url - await super.goto(shareUrl); + await super.goto(shareUrl); } async verifyPageLoaded(): Promise { diff --git a/ui/tests/sign-up/sign-up.md b/ui/tests/sign-up/sign-up.md index d82276a5f3..8344a8dd43 100644 --- a/ui/tests/sign-up/sign-up.md +++ b/ui/tests/sign-up/sign-up.md @@ -10,17 +10,20 @@ **Priority:** `critical` **Tags:** + - type → @e2e - feature → @signup **Description/Objetive:** Registers a new user with valid data, verifies redirect to Login (OSS), and confirms the user can authenticate. **Preconditions:** + - Application is running, email domain & password is acceptable for sign-up. - No existing data in Prowler is required; the test can run on a clean state. - `E2E_NEW_USER_PASSWORD` environment variable must be set with a valid password for the test. ### Flow Steps: + 1. Navigate to the Sign up page. 2. Fill the form with valid data (unique email, valid password, terms accepted). 3. Submit the form. @@ -28,16 +31,17 @@ 5. Log in with the newly created credentials. ### Expected Result: + - Sign-up succeeds and redirects to Login. - User can log in successfully using the created credentials and reach the home page. ### Key verification points: + - After submitting sign-up, the URL changes to `/sign-in`. - The newly created credentials can be used to sign in successfully. - After login, the user lands on the home (`/`) and main content is visible. ### Notes: + - Test data uses a random base36 suffix to avoid collisions with email. - The test requires the `E2E_NEW_USER_PASSWORD` environment variable to be set before running. - - diff --git a/ui/tests/sign-up/sign-up.spec.ts b/ui/tests/sign-up/sign-up.spec.ts index 9114adaa6d..3c732c6497 100644 --- a/ui/tests/sign-up/sign-up.spec.ts +++ b/ui/tests/sign-up/sign-up.spec.ts @@ -11,7 +11,9 @@ test.describe("Sign Up Flow", () => { const password = process.env.E2E_NEW_USER_PASSWORD; if (!password) { - throw new Error("E2E_NEW_USER_PASSWORD environment variable is not set"); + throw new Error( + "E2E_NEW_USER_PASSWORD environment variable is not set", + ); } const signUpPage = new SignUpPage(page); diff --git a/ui/tsconfig.json b/ui/tsconfig.json index e90024a945..40977d892c 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -6,19 +6,13 @@ "incremental": true, "isolatedModules": true, "jsx": "react-jsx", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "module": "esnext", "moduleResolution": "bundler", "noEmit": true, "baseUrl": ".", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] }, "plugins": [ { @@ -30,10 +24,7 @@ "strict": true, "target": "es5" }, - "exclude": [ - "node_modules", - "vitest.config.ts" - ], + "exclude": ["node_modules", "vitest.config.ts"], "include": [ "next-env.d.ts", "**/*.ts",