mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-13 15:50:55 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b4393776c |
@@ -36,6 +36,7 @@ Please add a detailed description of how to review this PR.
|
||||
|
||||
#### UI
|
||||
- [ ] All issue/task requirements work as expected on the UI
|
||||
- [ ] If this PR adds or updates npm dependencies, include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and explain why existing/native alternatives are insufficient.
|
||||
- [ ] Screenshots/Video of the functionality flow (if applicable) - Mobile (X < 640px)
|
||||
- [ ] Screenshots/Video of the functionality flow (if applicable) - Table (640px > X < 1024px)
|
||||
- [ ] Screenshots/Video of the functionality flow (if applicable) - Desktop (X > 1024px)
|
||||
|
||||
@@ -132,6 +132,10 @@ jobs:
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pnpm run healthcheck
|
||||
|
||||
- name: Run pnpm audit
|
||||
if: steps.check-changes.outputs.any_changed == 'true'
|
||||
run: pnpm run audit
|
||||
|
||||
- name: Run unit tests (all - critical paths changed)
|
||||
if: steps.check-changes.outputs.any_changed == 'true' && steps.critical-changes.outputs.any_changed == 'true'
|
||||
run: |
|
||||
|
||||
+2
-2
@@ -10,10 +10,10 @@ This repository contains the Prowler Open Source documentation powered by [Mintl
|
||||
|
||||
## Local Development
|
||||
|
||||
Install the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview documentation changes locally:
|
||||
Install a reviewed version of the [Mintlify CLI](https://www.npmjs.com/package/mint) to preview documentation changes locally:
|
||||
|
||||
```bash
|
||||
npm i -g mint
|
||||
npm install --global mint@4.2.560
|
||||
```
|
||||
|
||||
Run the following command at the root of your documentation (where `mint.json` is located):
|
||||
|
||||
@@ -28,7 +28,7 @@ This includes the [AGENTS.md](https://github.com/prowler-cloud/prowler/blob/mast
|
||||
<Steps>
|
||||
<Step title="Install Mintlify CLI">
|
||||
```bash
|
||||
npm i -g mint
|
||||
npm install --global mint@4.2.560
|
||||
```
|
||||
For detailed instructions, check the [Mintlify documentation](https://www.mintlify.com/docs/installation).
|
||||
</Step>
|
||||
|
||||
@@ -44,13 +44,21 @@ Choose the configuration based on your deployment:
|
||||
|
||||
<Tab title="Generic without Native HTTP Support">
|
||||
**Configuration:**
|
||||
<Warning>
|
||||
Avoid configuring MCP clients to run `npx mcp-remote` directly. `npx` can download and execute a new package version on each run. Install a reviewed version of `mcp-remote` in a dedicated local workspace, then point the MCP client to the installed binary.
|
||||
</Warning>
|
||||
```bash
|
||||
mkdir -p ~/.local/share/prowler-mcp-bridge
|
||||
cd ~/.local/share/prowler-mcp-bridge
|
||||
npm init -y
|
||||
npm install --save-exact mcp-remote@0.1.38
|
||||
```
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"command": "npx",
|
||||
"command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://mcp.prowler.com/mcp", // or your self-hosted Prowler MCP Server URL
|
||||
"--header",
|
||||
"Authorization: Bearer ${PROWLER_APP_API_KEY}"
|
||||
@@ -72,14 +80,20 @@ Choose the configuration based on your deployment:
|
||||
2. Go to "Developer" tab
|
||||
3. Click in "Edit Config" button
|
||||
4. Edit the `claude_desktop_config.json` file with your favorite editor
|
||||
5. Add the following configuration:
|
||||
5. Install a reviewed version of `mcp-remote` in a dedicated local workspace:
|
||||
```bash
|
||||
mkdir -p ~/.local/share/prowler-mcp-bridge
|
||||
cd ~/.local/share/prowler-mcp-bridge
|
||||
npm init -y
|
||||
npm install --save-exact mcp-remote@0.1.38
|
||||
```
|
||||
6. Add the following configuration:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"command": "npx",
|
||||
"command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://mcp.prowler.com/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer ${PROWLER_APP_API_KEY}"
|
||||
|
||||
@@ -38,7 +38,7 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
|
||||
|
||||
- `git` installed.
|
||||
- `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation).
|
||||
- `npm` installed: [npm installation](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm).
|
||||
- `pnpm` installed through [Corepack](https://pnpm.io/installation#using-corepack) or the standalone [pnpm installation](https://pnpm.io/installation).
|
||||
- `Docker Compose` installed: https://docs.docker.com/compose/install/.
|
||||
|
||||
<Warning>
|
||||
@@ -97,9 +97,11 @@ Refer to the [Prowler App Tutorial](/user-guide/tutorials/prowler-app) for detai
|
||||
```bash
|
||||
git clone https://github.com/prowler-cloud/prowler \
|
||||
cd prowler/ui \
|
||||
npm install \
|
||||
npm run build \
|
||||
npm start
|
||||
corepack enable \
|
||||
corepack install \
|
||||
pnpm install --frozen-lockfile \
|
||||
pnpm run build \
|
||||
pnpm start
|
||||
```
|
||||
|
||||
> Enjoy Prowler App at http://localhost:3000 by signing up with your email and password.
|
||||
|
||||
@@ -22,7 +22,7 @@ Install promptfoo using one of the following methods:
|
||||
|
||||
**Using npm:**
|
||||
```bash
|
||||
npm install -g promptfoo
|
||||
npm install --global promptfoo@0.121.11
|
||||
```
|
||||
|
||||
**Using Homebrew (macOS):**
|
||||
|
||||
+10
-2
@@ -56,13 +56,21 @@ Prowler MCP Server can be used in three ways:
|
||||
- Managed and maintained by Prowler team
|
||||
- Always up-to-date
|
||||
|
||||
Install a reviewed version of `mcp-remote` in a dedicated local workspace first. Avoid running `npx mcp-remote` directly because it can download and execute a new package version on each run.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.local/share/prowler-mcp-bridge
|
||||
cd ~/.local/share/prowler-mcp-bridge
|
||||
npm init -y
|
||||
npm install --save-exact mcp-remote@0.1.38
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"prowler": {
|
||||
"command": "npx",
|
||||
"command": "/absolute/path/to/.local/share/prowler-mcp-bridge/node_modules/.bin/mcp-remote",
|
||||
"args": [
|
||||
"mcp-remote",
|
||||
"https://mcp.prowler.com/mcp",
|
||||
"--header",
|
||||
"Authorization: Bearer pk_YOUR_API_KEY_HERE"
|
||||
|
||||
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!/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
|
||||
+18
-19
@@ -3,30 +3,21 @@ 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: 10
|
||||
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
|
||||
|
||||
- id: ui-tests
|
||||
name: UI - Unit Tests
|
||||
@@ -35,4 +26,12 @@ repos:
|
||||
files: '\.(ts|tsx|js|jsx)$'
|
||||
exclude: '\.test\.|\.spec\.|vitest\.config|vitest\.setup'
|
||||
pass_filenames: true
|
||||
priority: 10
|
||||
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
|
||||
|
||||
+1
-12
@@ -1,12 +1 @@
|
||||
node_modules/
|
||||
.next/
|
||||
build/
|
||||
dist/
|
||||
esm/
|
||||
coverage/
|
||||
.now/
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
next-env.d.ts
|
||||
public/
|
||||
node_modules/
|
||||
+34
-38
@@ -1,7 +1,6 @@
|
||||
# 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
|
||||
@@ -19,35 +18,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` |
|
||||
|
||||
---
|
||||
|
||||
@@ -138,8 +137,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) });
|
||||
@@ -164,12 +163,8 @@ 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 }) => {
|
||||
@@ -231,5 +226,6 @@ pnpm run test:e2e:ui
|
||||
- [ ] Relevant E2E tests pass
|
||||
- [ ] All UI states handled (loading, error, empty)
|
||||
- [ ] No secrets in code (use `.env.local`)
|
||||
- [ ] New npm dependencies include package-health evidence (maintenance, popularity, known vulnerabilities, license, release age) and a rationale for not using existing/native alternatives.
|
||||
- [ ] Error messages sanitized
|
||||
- [ ] Server-side validation present
|
||||
|
||||
@@ -11,7 +11,6 @@ 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 [(#11118)](https://github.com/prowler-cloud/prowler/pull/11118)
|
||||
- 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)
|
||||
|
||||
|
||||
+40
-20
@@ -2,12 +2,10 @@
|
||||
|
||||
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
|
||||
@@ -16,21 +14,16 @@ 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
|
||||
@@ -55,11 +48,8 @@ pnpm start
|
||||
```
|
||||
|
||||
## 🧪 Development deployment
|
||||
|
||||
### Docker deployment
|
||||
|
||||
#### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
git clone https://github.com/prowler-cloud/ui.git
|
||||
@@ -68,21 +58,16 @@ 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
|
||||
@@ -122,12 +107,47 @@ pnpm run dev
|
||||
- [Framer Motion](https://www.framer.com/motion/)
|
||||
- [next-themes](https://github.com/pacocoursey/next-themes)
|
||||
|
||||
## Git Hooks
|
||||
## Git Hooks & Code Review
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```bash
|
||||
prek install --overwrite
|
||||
CODE_REVIEW_ENABLED=true
|
||||
```
|
||||
|
||||
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.
|
||||
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
|
||||
```
|
||||
|
||||
@@ -81,7 +81,9 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
{requirement.remediation_procedure &&
|
||||
typeof requirement.remediation_procedure === "string" && (
|
||||
<ComplianceDetailSection title="Remediation Procedure">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
{/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */}
|
||||
{/* eslint-disable-next-line */}
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown>{requirement.remediation_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
@@ -90,7 +92,8 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
{requirement.audit_procedure &&
|
||||
typeof requirement.audit_procedure === "string" && (
|
||||
<ComplianceDetailSection title="Audit Procedure">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
||||
{/* eslint-disable-next-line */}
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
<ReactMarkdown>{requirement.audit_procedure}</ReactMarkdown>
|
||||
</div>
|
||||
</ComplianceDetailSection>
|
||||
|
||||
@@ -109,10 +109,10 @@ export function MyComponent() {
|
||||
|
||||
## Adding New shadcn Components
|
||||
|
||||
When adding new shadcn components using the CLI:
|
||||
When adding new shadcn components using the CLI, pin the reviewed CLI version instead of using `@latest`:
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add [component-name]
|
||||
pnpm dlx shadcn@4.7.0 add [component-name]
|
||||
```
|
||||
|
||||
The component will be automatically added to this directory due to the configuration in `components.json`:
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
# 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
|
||||
@@ -0,0 +1,241 @@
|
||||
# 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
|
||||
@@ -41,9 +41,6 @@ export default [
|
||||
// TypeScript and React files configuration
|
||||
{
|
||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: "error",
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": tsPlugin,
|
||||
"@next/next": nextPlugin,
|
||||
|
||||
+6
-4
@@ -10,12 +10,12 @@
|
||||
"postinstall": "node scripts/postinstall.js",
|
||||
"typecheck": "tsc",
|
||||
"healthcheck": "pnpm run typecheck && pnpm run lint:check",
|
||||
"lint:check": "eslint . --max-warnings 40",
|
||||
"lint:fix": "eslint . --fix --max-warnings 40",
|
||||
"lint:check": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:knip": "knip --max-issues 494",
|
||||
"lint:knip:fix": "knip --fix --max-issues 494",
|
||||
"format:check": "./node_modules/.bin/prettier --check .",
|
||||
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write .",
|
||||
"format:check": "./node_modules/.bin/prettier --check ./app",
|
||||
"format:write": "./node_modules/.bin/prettier --config .prettierrc.json --write ./app",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:unit": "vitest run --project unit",
|
||||
@@ -28,6 +28,8 @@
|
||||
"test:e2e:headed": "playwright test --project=auth --project=sign-up --project=providers --project=invitations --project=scans --headed",
|
||||
"test:e2e:report": "playwright show-report",
|
||||
"test:e2e:install": "playwright install",
|
||||
"audit": "pnpm audit --audit-level critical",
|
||||
"audit:high": "pnpm audit --audit-level high",
|
||||
"audit:fix": "pnpm audit fix"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -14,20 +14,21 @@ minimumReleaseAge: 1440
|
||||
|
||||
# --- Level 2: Explicit Build Script Allow-list ---
|
||||
# Only these packages may run install/postinstall lifecycle scripts.
|
||||
# Any unlisted package with lifecycle scripts will have them silently skipped.
|
||||
onlyBuiltDependencies:
|
||||
# Any unlisted package with lifecycle scripts fails the install.
|
||||
strictDepBuilds: true
|
||||
allowBuilds:
|
||||
# sharp: Native image processing (libvips). Installs platform-specific pre-built binary or compiles from source.
|
||||
- sharp
|
||||
sharp: true
|
||||
# @sentry/cli: Downloads the sentry-cli native binary for the current platform. Validates integrity via SHA256.
|
||||
- "@sentry/cli"
|
||||
"@sentry/cli": true
|
||||
# esbuild: Go binary. Downloads the pre-compiled binary matching the current platform/architecture.
|
||||
- esbuild
|
||||
esbuild: true
|
||||
# @heroui/shared-utils: Demi pattern — detects React/Next.js version at install time and copies the compatible bundle (React 18 vs 19).
|
||||
- "@heroui/shared-utils"
|
||||
"@heroui/shared-utils": true
|
||||
# unrs-resolver: Rust module resolver (NAPI-RS). Verifies the correct native binding is available for the platform.
|
||||
- unrs-resolver
|
||||
unrs-resolver: true
|
||||
# msw: Copies mockServiceWorker.js into the directories listed in package.json's `msw.workerDirectory` (here: `public/`) so the runtime worker stays in sync with the installed msw version. Pure file copy — no native binary, no network access. Required for vitest browser tests to intercept fetches via the service worker.
|
||||
- msw
|
||||
msw: true
|
||||
|
||||
# --- Level 3: Trust Policy + Exotic Subdeps ---
|
||||
# Fail when a package's trust evidence is downgraded (e.g., new publisher).
|
||||
|
||||
@@ -7,25 +7,21 @@
|
||||
* 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;
|
||||
}
|
||||
@@ -36,9 +32,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;
|
||||
@@ -49,61 +45,51 @@ 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);
|
||||
}
|
||||
|
||||
@@ -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,37 +25,35 @@ 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);
|
||||
}
|
||||
@@ -80,10 +78,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;
|
||||
|
||||
+1
-2
@@ -45,9 +45,8 @@ 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)
|
||||
- `404` - Not Found (expected for missing resources)
|
||||
@@ -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 */
|
||||
|
||||
@@ -85,15 +85,18 @@ 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();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -10,19 +10,16 @@
|
||||
**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.
|
||||
@@ -30,7 +27,6 @@
|
||||
5. Verify page loads.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- Public routes are accessible without authentication.
|
||||
|
||||
---
|
||||
@@ -40,18 +36,15 @@
|
||||
**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).
|
||||
@@ -59,7 +52,6 @@
|
||||
5. Verify redirect to sign-in.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- Invalid session results in redirect to sign-in.
|
||||
|
||||
---
|
||||
@@ -69,24 +61,20 @@
|
||||
**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.
|
||||
|
||||
@@ -97,24 +85,20 @@
|
||||
**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.
|
||||
|
||||
@@ -125,23 +109,19 @@
|
||||
**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".
|
||||
|
||||
---
|
||||
@@ -151,19 +131,16 @@
|
||||
**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).
|
||||
@@ -172,7 +149,6 @@
|
||||
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.
|
||||
|
||||
@@ -183,19 +159,16 @@
|
||||
**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.
|
||||
@@ -203,7 +176,6 @@
|
||||
5. Verify session is still valid with same user data.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- Session persists after reload.
|
||||
- User email, userId, and tenantId remain the same.
|
||||
|
||||
@@ -214,26 +186,22 @@
|
||||
**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.
|
||||
|
||||
@@ -244,25 +212,21 @@
|
||||
**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.
|
||||
|
||||
+13
-27
@@ -77,17 +77,11 @@ export abstract class BasePage {
|
||||
await expect(element).not.toBeVisible();
|
||||
}
|
||||
|
||||
async verifyElementText(
|
||||
element: Locator,
|
||||
expectedText: string,
|
||||
): Promise<void> {
|
||||
async verifyElementText(element: Locator, expectedText: string): Promise<void> {
|
||||
await expect(element).toHaveText(expectedText);
|
||||
}
|
||||
|
||||
async verifyElementContainsText(
|
||||
element: Locator,
|
||||
expectedText: string,
|
||||
): Promise<void> {
|
||||
async verifyElementContainsText(element: Locator, expectedText: string): Promise<void> {
|
||||
await expect(element).toContainText(expectedText);
|
||||
}
|
||||
|
||||
@@ -99,9 +93,7 @@ export abstract class BasePage {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyAriaLabels(
|
||||
elements: { locator: Locator; expectedLabel: string }[],
|
||||
): Promise<void> {
|
||||
async verifyAriaLabels(elements: { locator: Locator; expectedLabel: string }[]): Promise<void> {
|
||||
for (const { locator, expectedLabel } of elements) {
|
||||
await expect(locator).toHaveAttribute("aria-label", expectedLabel);
|
||||
}
|
||||
@@ -109,7 +101,7 @@ export abstract class BasePage {
|
||||
|
||||
// Common utility methods
|
||||
async getElementText(element: Locator): Promise<string> {
|
||||
return (await element.textContent()) || "";
|
||||
return await element.textContent() || "";
|
||||
}
|
||||
|
||||
async getElementValue(element: Locator): Promise<string> {
|
||||
@@ -126,9 +118,7 @@ export abstract class BasePage {
|
||||
|
||||
// Common error handling methods
|
||||
async getFormErrors(): Promise<string[]> {
|
||||
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) {
|
||||
@@ -147,33 +137,29 @@ export abstract class BasePage {
|
||||
}
|
||||
|
||||
// Common wait methods
|
||||
async waitForElement(
|
||||
element: Locator,
|
||||
timeout: number = 5000,
|
||||
): Promise<void> {
|
||||
async waitForElement(element: Locator, timeout: number = 5000): Promise<void> {
|
||||
|
||||
await element.waitFor({ timeout });
|
||||
}
|
||||
|
||||
async waitForElementToDisappear(
|
||||
element: Locator,
|
||||
timeout: number = 5000,
|
||||
): Promise<void> {
|
||||
async waitForElementToDisappear(element: Locator, timeout: number = 5000): Promise<void> {
|
||||
|
||||
await element.waitFor({ state: "hidden", timeout });
|
||||
}
|
||||
|
||||
async waitForUrl(
|
||||
expectedUrl: string | RegExp,
|
||||
timeout: number = 5000,
|
||||
): Promise<void> {
|
||||
async waitForUrl(expectedUrl: string | RegExp, timeout: number = 5000): Promise<void> {
|
||||
|
||||
await this.page.waitForURL(expectedUrl, { timeout });
|
||||
}
|
||||
|
||||
// Common screenshot methods
|
||||
async takeScreenshot(name: string): Promise<void> {
|
||||
|
||||
await this.page.screenshot({ path: `screenshots/${name}.png` });
|
||||
}
|
||||
|
||||
async takeElementScreenshot(element: Locator, name: string): Promise<void> {
|
||||
|
||||
await element.screenshot({ path: `screenshots/${name}.png` });
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -1,10 +1,5 @@
|
||||
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 = {
|
||||
@@ -75,6 +70,7 @@ export async function verifySessionValid(page: Page) {
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
export async function addAWSProvider(
|
||||
page: Page,
|
||||
accountId: string,
|
||||
@@ -132,10 +128,7 @@ export async function addAWSProvider(
|
||||
await scansPage.verifyPageLoaded();
|
||||
}
|
||||
|
||||
export async function deleteProviderIfExists(
|
||||
page: ProvidersPage,
|
||||
providerUID: string,
|
||||
): Promise<void> {
|
||||
export async function deleteProviderIfExists(page: ProvidersPage, providerUID: string): Promise<void> {
|
||||
// Delete the provider if it exists
|
||||
|
||||
// Navigate to providers page
|
||||
@@ -185,7 +178,11 @@ export async function deleteProviderIfExists(
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
@@ -2,6 +2,7 @@ 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;
|
||||
@@ -25,10 +26,7 @@ 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");
|
||||
|
||||
@@ -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
|
||||
- The role `e2e_admin` must be available in the environment
|
||||
@@ -944,7 +944,9 @@ 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()
|
||||
|
||||
@@ -3,35 +3,29 @@ 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 provider selection elements
|
||||
readonly scanProviderSelect: Locator;
|
||||
readonly scanAliasInput: Locator;
|
||||
readonly startNowButton: Locator;
|
||||
|
||||
// Scan state elements
|
||||
readonly successToast: 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");
|
||||
@@ -76,9 +70,7 @@ 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();
|
||||
@@ -94,6 +86,7 @@ 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);
|
||||
@@ -105,6 +98,7 @@ export class ScansPage extends BasePage {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async verifyScheduledScanStatus(accountId: string): Promise<void> {
|
||||
// Verifies that:
|
||||
// 1. The provider exists in the table (by account ID/UID)
|
||||
@@ -139,4 +133,5 @@ export class ScansPage extends BasePage {
|
||||
ignoreCase: true,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -51,3 +51,5 @@
|
||||
- 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.
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ test.describe("Scans", () => {
|
||||
tag: ["@e2e", "@scans", "@critical", "@serial", "@SCAN-E2E-001"],
|
||||
},
|
||||
async ({ page }) => {
|
||||
|
||||
const accountId = process.env.E2E_AWS_PROVIDER_ACCOUNT_ID;
|
||||
|
||||
if (!accountId) {
|
||||
@@ -62,6 +63,8 @@ test.describe("Scans", () => {
|
||||
|
||||
// Verify the scan was launched
|
||||
await scansPage.verifyScanLaunched("E2E Test Scan - On Demand");
|
||||
|
||||
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
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";
|
||||
const adminUserFile = 'playwright/.auth/admin_user.json';
|
||||
|
||||
authAdminSetup('authenticate as admin e2e user', async ({ page }) => {
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
await signInPage.authenticateAndSaveState({ email: adminEmail, password: adminPassword }, adminUserFile);
|
||||
});
|
||||
@@ -1,30 +1,16 @@
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.authenticateAndSaveState({ email: inviteAndManageUsersEmail, password: inviteAndManageUsersPassword }, inviteAndManageUsersUserFile);
|
||||
});
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.authenticateAndSaveState({ email: accountEmail, password: accountPassword }, manageAccountUserFile);
|
||||
});
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.authenticateAndSaveState({ email: cloudProvidersEmail, password: cloudProvidersPassword }, manageCloudProvidersUserFile);
|
||||
});
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.authenticateAndSaveState({ email: integrationsEmail, password: integrationsPassword }, manageIntegrationsUserFile);
|
||||
});
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
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);
|
||||
});
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
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;
|
||||
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');
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
const signInPage = new SignInPage(page);
|
||||
await signInPage.authenticateAndSaveState({ email: unlimitedVisibilityEmail, password: unlimitedVisibilityPassword }, unlimitedVisibilityUserFile);
|
||||
});
|
||||
|
||||
@@ -10,18 +10,15 @@
|
||||
**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).
|
||||
@@ -29,7 +26,6 @@
|
||||
5. Verify navigation links.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- All form elements are visible and properly labeled.
|
||||
|
||||
---
|
||||
@@ -39,26 +35,22 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -68,25 +60,21 @@
|
||||
**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.
|
||||
|
||||
@@ -97,24 +85,20 @@
|
||||
**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.
|
||||
|
||||
@@ -125,25 +109,21 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -153,25 +133,21 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -181,18 +157,15 @@
|
||||
**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).
|
||||
@@ -200,7 +173,6 @@
|
||||
5. Verify normal mode is restored.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- SAML mode toggles correctly.
|
||||
- Password field visibility changes accordingly.
|
||||
|
||||
@@ -211,25 +183,21 @@
|
||||
**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).
|
||||
|
||||
---
|
||||
@@ -239,25 +207,21 @@
|
||||
**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).
|
||||
|
||||
---
|
||||
@@ -267,26 +231,22 @@
|
||||
**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.
|
||||
|
||||
@@ -297,24 +257,20 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -324,19 +280,16 @@
|
||||
**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.
|
||||
@@ -344,7 +297,6 @@
|
||||
5. Verify redirect to sign-in.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- User is logged out.
|
||||
- Session is invalidated.
|
||||
- Protected routes are no longer accessible.
|
||||
@@ -356,18 +308,15 @@
|
||||
**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.
|
||||
@@ -375,7 +324,6 @@
|
||||
5. Verify new context is redirected to sign-in.
|
||||
|
||||
### Expected Result:
|
||||
|
||||
- Sessions are isolated between contexts.
|
||||
- Unauthenticated context cannot access protected routes.
|
||||
|
||||
@@ -386,24 +334,20 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -413,24 +357,20 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -440,25 +380,21 @@
|
||||
**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.
|
||||
|
||||
---
|
||||
@@ -468,24 +404,20 @@
|
||||
**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.
|
||||
|
||||
@@ -496,23 +428,19 @@
|
||||
**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.
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface SignUpData {
|
||||
}
|
||||
|
||||
export class SignUpPage extends BasePage {
|
||||
|
||||
// Form inputs
|
||||
readonly nameInput: Locator;
|
||||
readonly companyInput: Locator;
|
||||
@@ -38,9 +39,7 @@ 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<void> {
|
||||
@@ -51,7 +50,7 @@ export class SignUpPage extends BasePage {
|
||||
async gotoInvite(shareUrl: string): Promise<void> {
|
||||
// Navigate to the share url
|
||||
|
||||
await super.goto(shareUrl);
|
||||
await super.goto(shareUrl);
|
||||
}
|
||||
|
||||
async verifyPageLoaded(): Promise<void> {
|
||||
|
||||
@@ -10,20 +10,17 @@
|
||||
**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.
|
||||
@@ -31,17 +28,16 @@
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ 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);
|
||||
|
||||
+12
-3
@@ -6,13 +6,19 @@
|
||||
"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": [
|
||||
{
|
||||
@@ -24,7 +30,10 @@
|
||||
"strict": true,
|
||||
"target": "es5"
|
||||
},
|
||||
"exclude": ["node_modules", "vitest.config.ts"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"vitest.config.ts"
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
|
||||
Reference in New Issue
Block a user