Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
42 KiB
Prowler UI Agent Guide
Complete guide for AI agents and developers working on the Prowler UI Next.js application.
Mission & Scope
- Ship small, high-impact UI changes with minimal risk
- Align to current patterns: App Router, Server Components first, consistent styling, strict types
- Avoid broad refactors, library swaps, or reorganization unless requested
- Focus on safe, incremental frontend changes aligned with existing architecture
Critical Architecture Rules (Non-Negotiable)
1. TypeScript Type Patterns (Required)
When defining union types for options, ALWAYS create a const object first, then extract the type:
❌ DON'T:
type SortOption = "high-low" | "low-high" | "alphabetical";
✅ DO:
const SORT_OPTIONS = {
HIGH_LOW: "high-low",
LOW_HIGH: "low-high",
ALPHABETICAL: "alphabetical",
} as const;
type SortOption = (typeof SORT_OPTIONS)[keyof typeof SORT_OPTIONS];
2. Tailwind 4 Theme Variables
This project uses Tailwind 4 with @theme variables. Tailwind is mainly semantic - prioritize using Tailwind's naming system whenever possible.
In Template/JSX (className)
- ✅ Use Tailwind utility classes:
bg-card-bg,text-white,text-slate-400,border-slate-700 - ✅ Use arbitrary values with classes:
h-3,w-3,min-w-[200px],bg-slate-700/50 - ✅ Use Tailwind for conditional styles:
className={isActive ? "bg-blue-500" : "bg-gray-500"} - ✅ Use style props only for truly dynamic values:
style={{ width: \${percentage}%` }}` - ❌ NEVER use
var()in className - ❌ NEVER use hex colors in className
Constants with var() (Only for library props that don't accept className)
- ✅ Use CHART_COLORS constants:
stroke={CHART_COLORS.gridLine},tick={{ fill: CHART_COLORS.textSecondary }} - These props don't accept className, so we use constants that internally reference
var() - This is the only valid use case for
var()- when the library doesn't support className
Examples
// ✅ GOOD - Template with Tailwind classes
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3">
<p className="text-sm font-semibold text-white">{title}</p>
<Bell size={14} className="text-slate-400" />
</div>
// ✅ GOOD - Conditional Tailwind classes
<button className={isActive ? "bg-blue-500" : "bg-gray-500"}>
Click me
</button>
// ✅ GOOD - Recharts library props with CHART_COLORS (var() only here)
<XAxis tick={{ fill: CHART_COLORS.textSecondary, fontSize: 12 }} />
<CartesianGrid stroke={CHART_COLORS.gridLine} />
// ✅ GOOD - Truly dynamic values (not available in Tailwind)
<div style={{ width: `${percentage}%`, opacity: isFaded ? 0.5 : 1 }} />
// ❌ BAD - var() in className
<div className="bg-[var(--color-card-bg)]" /> // Don't do this!
// ❌ BAD - Hex colors in className
<p className="text-[#ffffff]" /> // Use text-white instead
// ❌ BAD - Using var() for colors when Tailwind classes exist
const PROVIDER_COLORS = {
AWS: "var(--color-orange)", // Use Tailwind classes instead!
};
// ❌ BAD - Using style when className is available
<div style={{ backgroundColor: "blue" }} /> // Use className="bg-blue-500" instead
3. The cn() Utility Function
What is cn()?
The cn() function is a utility that combines clsx and tailwind-merge:
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Components:
clsx: Constructs conditional className strings (handles booleans, arrays, objects)twMerge: Intelligently merges Tailwind classes, resolving conflicts (e.g.,p-4+p-2→p-2)
When to Use cn()
Use cn() ONLY when you have:
1. Conditional Classes
// ✅ GOOD - Conditional logic
<div className={cn("h-3 w-3", isCircle ? "rounded-full" : "rounded-sm")} />
// ✅ GOOD - Boolean conditionals
<button className={cn("btn", isActive && "btn-active", isDisabled && "opacity-50")} />
2. Merging Props with Conflicting Classes
// ✅ GOOD - Component accepting className prop
interface ButtonProps {
className?: string;
}
function Button({ className }: ButtonProps) {
return <button className={cn("bg-blue-500 px-4 py-2", className)} />;
}
// Usage: <Button className="px-6" /> → Results in "px-6 py-2 bg-blue-500"
3. Dynamic String Interpolation
// ✅ GOOD - Dynamic values that need proper merging
<span className={cn(`text-${size}`, "font-semibold text-white")} />
When NOT to Use cn()
DO NOT use cn() for static classes without conditional logic:
// ❌ BAD - Unnecessary, no conditional logic
<div className={cn("rounded-lg border border-slate-700 bg-slate-800 p-3")} />
// ✅ GOOD - Just use className directly
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3" />
// ❌ BAD - No conflicts or conditionals
<div className={cn("flex items-center gap-2")} />
// ✅ GOOD - Static classes don't need cn()
<div className="flex items-center gap-2" />
Real-World Examples
// ❌ BAD - Overuse of cn()
function Tooltip({ active, payload }: any) {
return (
<div className={cn("rounded-lg border border-slate-700")}>
<div className={cn("flex items-center gap-2")}>
<div className={cn("h-3 w-3 rounded-sm")} />
<span className={cn("text-sm font-semibold text-white")}>
{payload.name}
</span>
</div>
</div>
);
}
// ✅ GOOD - Only use cn() where needed
function Tooltip({ active, payload, shape }: any) {
return (
<div className="rounded-lg border border-slate-700 bg-slate-800 p-3">
<div className="flex items-center gap-2">
<div
className={cn(
"h-3 w-3",
shape === "circle" ? "rounded-full" : "rounded-sm",
)}
/>
<span className="text-sm font-semibold text-white">{payload.name}</span>
</div>
</div>
);
}
Key Takeaway
cn() is a tool for conditional logic and conflict resolution, NOT a wrapper for every className.
Use it purposefully where it adds value. Don't use it out of habit.
Reference: The Story Behind Tailwind's cn() Function
4. React 19 with Compiler
This project uses React 19 with the React Compiler enabled. This means:
- DO NOT use
useMemo- React Compiler handles memoization automatically - DO NOT use
useCallback- React Compiler optimizes callbacks automatically - Only use these hooks if you have a specific, documented reason that the compiler cannot handle
5. Next.js 15 Architecture Principles
1. App Router Architecture First
- ALL routes MUST use App Router - never use Pages Router for new projects
- Leverage Server Components by default, Client Components only when necessary
- Use proper file conventions:
page.tsx,layout.tsx,loading.tsx,error.tsx,not-found.tsx - Implement route groups
(group-name)for organization without affecting URL structure - Use private folders
_folderto opt out of routing system
2. Server-First Architecture
- Server Components by default - add
"use client"only when required - Optimize data fetching at the server level
- Implement streaming with
loading.tsxand Suspense boundaries - Use Server Actions for form handling and mutations
- Leverage static generation and ISR for performance
- Use DAL (Data Access Layer) patterns to separate data logic
- To prevent accidental usage in Client Components, you can use the server-only package, this is a MUST for Server Actions and recommended for all server-only code
3. The Scope Rule - Your Unbreakable Law
"Scope determines structure"
- Code used by 2+ features → MUST go in global/shared directories
- Code used by 1 feature → MUST stay local in that feature
- NO EXCEPTIONS - This rule is absolute and non-negotiable
4. Screaming Architecture
Your structures must IMMEDIATELY communicate what the application does:
- Feature names must describe business functionality, not technical implementation
- Directory structure should tell the story of what the app does at first glance
- Route structure should mirror business logic, not technical concerns
5. Component Placement Decision Framework
When analyzing where to place a component, you MUST follow this process:
- Identify component type: Server Component, Client Component, or hybrid
- Count usage: Identify exactly how many features/routes use the component
- Apply the Scope Rule:
- Used by 1 feature → Local placement within that feature
- Used by 2+ features → Global/shared directory
- Consider performance: Optimize bundle splitting and server-side rendering
- Document the decision: Always explain WHY the placement was chosen
6. File Organization Patterns
Co-location Principles
Follow these patterns for organizing related files:
For Server Actions:
ui/actions/
└── feature-name/
├── feature-name.ts # Server actions
├── models.ts # Domain models/types
└── feature-name.adapter.ts # Data transformations (if needed)
For Components:
ui/components/
└── feature-name/
├── feature-component.tsx # Main component
├── feature-client.tsx # Client-specific logic
└── feature-name/ # Utilities folder
├── types.ts
├── utils.ts
├── constants.ts
└── hooks.ts
Key Rules:
- Models used by only one feature → Keep local in that feature's directory
- Utilities used by only one feature → Keep in feature's utils folder
- Types only used within a feature → Keep in feature's types file
- Only move to global/shared when used by 2+ features
Project Overview
The Prowler UI is a Next.js 15 application providing a modern web interface for the Prowler security platform. It features a comprehensive dashboard for managing cloud security scans, compliance frameworks, and findings across multiple cloud providers.
Tech Stack (Updated January 2025)
- Framework: Next.js 15.5.3 with App Router
- Runtime: React 19.1.1 (with Compiler enabled)
- Language: TypeScript 5.5.4
- Styling: Tailwind CSS 4.1.13 + shadcn/ui (new components) / HeroUI 2.8.4 (legacy)
- State Management: Zustand 5.0.8
- Authentication: NextAuth.js 5.0.0-beta.29
- Forms/Validation: React Hook Form 7.62.0 + Zod 4.1.11
- AI/Chat: AI SDK 5.0.59 + @ai-sdk/react 2.0.59
- AI Backend: LangChain @langchain/core 0.3.77 + @ai-sdk/langchain 1.0.59
- Charts: Recharts 2.15.4
- Testing: Playwright 1.53.2
- Formatter: Prettier 3.6.2
Commands
Development
npm install # Install dependencies
npm run dev # Start development server (localhost:3000)
npm run build # Build for production
npm start # Start production server
npm run start:standalone # Start standalone server
Code Quality
npm run typecheck # TypeScript type checking
npm run lint:check # ESLint checking
npm run lint:fix # Fix ESLint issues
npm run format:check # Prettier format checking
npm run format:write # Format code with Prettier
npm run healthcheck # Run typecheck + lint together
Testing
npm run test:e2e # Run Playwright tests
npm run test:e2e:ui # Run tests with UI
npm run test:e2e:debug # Debug tests
npm run test:e2e:headed # Run tests in headed mode
npm run test:e2e:report # Show test report
npm run test:e2e:install # Install Playwright browsers
Project Structure
ui/
├── app/ # Next.js App Router
│ ├── (auth)/ # Authentication pages (sign-in, sign-up)
│ ├── (prowler)/ # Main application pages
│ │ ├── compliance/ # Compliance frameworks & reports
│ │ ├── findings/ # Security findings & vulnerabilities
│ │ ├── integrations/ # S3, Security Hub integrations
│ │ ├── lighthouse/ # AI-powered security assistant
│ │ ├── providers/ # Cloud provider management
│ │ ├── scans/ # Security scan management
│ │ └── services/ # Cloud services overview
│ └── api/ # API routes & server actions
├── components/ # Reusable UI components
│ ├── shadcn/ # shadcn/ui components (NEW)
│ │ ├── card.tsx # shadcn Card component
│ │ ├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
│ │ │ ├── resource-stats-card.tsx
│ │ │ ├── resource-stats-card.example.tsx
│ │ │ └── index.ts
│ │ ├── index.ts # Barrel exports
│ │ └── README.md
│ ├── ui/ # Base UI components (buttons, forms, etc.)
│ ├── compliance/ # Compliance-specific components
│ ├── findings/ # Findings table & filters
│ ├── providers/ # Provider management UI
│ ├── scans/ # Scan management UI
│ └── integrations/ # Integration configuration
├── actions/ # Server actions (data fetching/mutations)
├── lib/ # Utility functions & configurations
├── types/ # TypeScript type definitions
├── hooks/ # Custom React hooks
├── store/ # Zustand state management
├── tests/ # Playwright E2E tests
└── styles/ # Global CSS & Tailwind config
shadcn/ui Components
Directory Structure
All shadcn/ui based components are located in components/shadcn/:
shadcn/
├── card.tsx # shadcn Card component
├── resource-stats-card/ # Custom ResourceStatsCard built on shadcn
│ ├── resource-stats-card.tsx
│ ├── resource-stats-card.example.tsx
│ └── index.ts
├── index.ts # Barrel exports
└── README.md
Usage
All shadcn components can be imported from @/components/shadcn:
import { Card, CardHeader, CardContent } from "@/components/shadcn";
import { ResourceStatsCard } from "@/components/shadcn";
Adding New shadcn Components
When adding new shadcn components using the CLI:
npx shadcn@latest add [component-name]
The component will be automatically added to this directory due to the configuration in components.json:
{
"aliases": {
"ui": "@/components/shadcn"
}
}
Component Guidelines
- shadcn base components - Use as-is from shadcn/ui (e.g.,
card.tsx) - Custom components built on shadcn - Create in subdirectories (e.g.,
resource-stats-card/) - CVA variants - Use Class Variance Authority for type-safe variants
- Theme support - Include
dark:classes for dark/light theme compatibility - TypeScript - Always export types and use proper typing
Resources
Key Features
Authentication System
- NextAuth.js with multiple providers (credentials, OAuth, SAML)
- Server-side authentication with middleware protection
- Session management and role-based access control
Cloud Provider Management
- Multi-cloud support (AWS, Azure, GCP, GitHub, K8s, M365)
- Credential management with secure forms
- Connection testing and status monitoring
Security Scanning
- Real-time scan progress monitoring
- Bulk operations and filtering
- Scheduled scan management
- Download and export capabilities
Compliance Frameworks
- 36+ compliance frameworks (CIS, NIST, PCI-DSS, etc.)
- Interactive compliance reports with charts
- Requirement-level drill-down views
- Custom compliance mapping
Lighthouse AI Assistant
- LangChain-powered security chatbot
- Context-aware responses about findings
- Integration with scan data and compliance frameworks
Findings Management
- Advanced filtering and search
- Mute/unmute functionality with reasons
- Severity-based classification
- Detailed finding analysis
Patterns & Conventions
Component Architecture
// Prefer server components when possible
export default async function PageComponent() {
const data = await fetchData();
return <ClientComponent data={data} />;
}
State Management
// Use Zustand for global state
import { useStore } from "@/hooks/use-store";
const { filters, setFilters } = useStore();
Server Actions
"use server";
export async function updateProvider(formData: FormData) {
// Validate with Zod
// Update via API
// Revalidate cache
}
Form Handling
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const form = useForm({
resolver: zodResolver(schema),
});
File & Code Style
Naming & Organization
- Component Naming:
PascalCasefor components,camelCasefor helpers - Foldering: Colocate domain components under domain folders (e.g.,
components/integrations/jira/) - Imports: Honor alias paths (
@/components/...). Keep import order consistent with ESLint rules - CSS: Prefer Tailwind classes; avoid ad-hoc CSS files unless justified
Import Organization
// External libraries
import React from "react";
import { Button } from "@heroui/react";
// Internal utilities
import { cn } from "@/lib/utils";
// Types
import type { ComponentProps } from "@/types";
Styling Guidelines
Tailwind + shadcn/ui (New) / HeroUI (Existing)
- Use shadcn/ui components for new UI features/pages with the new Tailwind theme
- Existing features/pages should continue using HeroUI (migrated from NextUI) for consistency
- Custom Prowler color palette defined in tailwind.config.js
- Dark/light theme support via next-themes
- Custom shadows and animations for Prowler brand
Library-Specific Guidelines
Zod v4 (Schema Validation)
Breaking changes from v3:
- ❌
.nonempty()→ ✅.min(1)for strings - ❌
z.string().email()→ ✅z.email()(top-level function) - ❌
z.string().uuid()→ ✅z.uuid()(top-level function) - ❌
z.string().url()→ ✅z.url()(top-level function) - ❌
required_errorparameter → ✅errorparameter - ❌
messageparameter → ✅errorparameter - ⚠️
.optional()type inference changed - fields are nowT | undefinedin inferred types
Example migration:
// ❌ Zod v3
const schema = z.object({
email: z.string().email({ message: "Invalid email" }),
name: z.string().nonempty("Required"),
id: z.string().uuid(),
});
// ✅ Zod v4
const schema = z.object({
email: z.email({ error: "Invalid email" }),
name: z.string().min(1, "Required"),
id: z.uuid(),
});
Zustand v5 (State Management)
Breaking changes from v4:
- ✅ No API changes required for basic usage
- ⚠️
shallowcomparison must useuseShallowhook fromzustand/react/shallow - ⚠️ Selectors must return stable references to avoid infinite loops
- ⚠️
persistmiddleware no longer auto-stores initial state - callsetState()explicitly if needed
Best practices:
import { create } from "zustand";
import { persist } from "zustand/middleware";
const useStore = create(
persist(
(set) => ({
value: 0,
increment: () => set((state) => ({ value: state.value + 1 })),
}),
{ name: "my-store" },
),
);
AI SDK v5 (Chat & AI Features)
Breaking changes from v4:
- ❌
Messagetype → ✅UIMessagetype - ❌
message.contentstring → ✅message.partsarray structure - ❌
handleSubmit/handleInputChange→ ✅sendMessagewith manual state - ❌
append()→ ✅sendMessage({ text: "..." }) - ❌
api: "/endpoint"→ ✅transport: new DefaultChatTransport({ api: "/endpoint" }) - ❌
LangChainAdapter.toDataStreamResponse()→ ✅toUIMessageStream()from@ai-sdk/langchain
Example migration:
// ❌ AI SDK v4
import { useChat } from "ai";
const { messages, handleSubmit, input, handleInputChange } = useChat({
api: "/api/chat",
});
// ✅ AI SDK v5
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
// Manual input state management required
const [input, setInput] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
};
UIMessage structure:
// Message parts-based structure
const message: UIMessage = {
id: "msg-1",
role: "assistant",
parts: [{ type: "text", text: "Hello world" }],
};
// Extract text from parts
const text = message.parts
.filter((p) => p.type === "text")
.map((p) => ("text" in p ? p.text : ""))
.join("");
Testing
Playwright E2E Tests
⚠️ MANDATORY: If you have access to Playwright MCP tools, ALWAYS use them to understand the actual application flow before creating any E2E test.
- IF Playwright MCP is available: Use browser tools to navigate, interact, and understand the real UI behavior FIRST, then create tests
- IF Playwright MCP is NOT available: Proceed with test creation based on available documentation and code analysis
- Add/update E2E tests for critical flows you modify
- Scope: run only affected specs when iterating
- Commit snapshot updates only with real UI changes
- Determinism: avoid relying on real external services; mock or stub where possible
- Organization: Create a folder under
tests/for each page (e.g.,tests/sign-in/,tests/sign-up/, etc.) - File Structure: Each page folder should contain 3 files:
{page-name}-page.ts- Page Object Model{page-name}.spec.ts- Test specifications{page-name}.md- Test documentation
- Base Class:
tests/base-page.ts- Parent class that all{page-name}-page.tsfiles should extend - Helpers:
tests/helpers.ts- Utility functions and helper methods for tests
Playwright MCP Integration
⚠️ CRITICAL WORKFLOW (When Available): If you have access to Playwright MCP browser tools, use them to explore the application BEFORE writing any test code.
Recommended Steps Before Creating Tests (Only if MCP Tools are Available):
- Navigate to the application to reach the target page
- Take a snapshot to see the page structure and available elements
- Interact with forms and elements to verify the exact user flow
- Take screenshots to document expected states at each step
- Verify page transitions by navigating through the complete flow to understand all states (loading, success, error)
- Document actual selectors from the snapshots - use the real element references (ref) and labels you observe
- Only after exploring the complete flow manually, create the test code with the exact selectors and steps you verified
Why This Matters (When MCP Tools are Available):
- ✅ Precise test creation - Only include the exact steps needed, no assumptions or guessing
- ✅ Accurate selectors - Use the actual DOM structure from real snapshots, not imagined selectors
- ✅ Real flow validation - Verify the complete user journey actually works as expected
- ✅ Avoid over-engineering - Create minimal tests that focus on what actually exists
- ✅ Prevent flaky tests - Tests based on real exploration are more stable and reliable
- ❌ Never assume - Don't create tests based on assumptions about how the UI "should" work
Benefits:
- Precise test creation - Only include the exact steps needed for the test requirement
- Accurate selectors - Use the actual DOM structure to create reliable locators
- Real flow validation - Verify the complete user journey works as expected
- Avoid over-engineering - Create minimal tests that focus on the specific requirement
Test Creation Guidelines
IMPORTANT: Always ask for clarification if the request is ambiguous about scope.
When creating a specific test:
- Create only a single
test()entry implementing the specific functionality described - Do NOT create the full test suite for this page
- ALWAYS add the test to the page's main spec file (e.g.,
sign-up.spec.ts), NOT in a separate file - REUSE existing page objects from other pages when possible (e.g., use existing SignInPage, HomePage, etc.)
- If the page's spec file doesn't exist, create minimal structure:
{page-name}-page.ts- Page Object Model{page-name}.spec.ts- Test specifications (add your specific test here)
- Focus on the exact requirement without additional test cases
- Do NOT create separate files like
{page-name}-critical-path.spec.tsor{page-name}-specific-test.spec.ts
When creating comprehensive page tests:
- Create the full test suite with all files (page object, spec, documentation)
- Include multiple test cases covering various scenarios in
{page-name}.spec.ts - Follow the complete structure with validation, error handling, accessibility tests
- Create comprehensive documentation for all test cases in
{page-name}.md
File Naming Convention:
- ✅ CORRECT:
sign-up.spec.ts(contains all sign-up tests) - ✅ CORRECT:
sign-up-page.ts(page object) - ✅ CORRECT:
sign-up.md(documentation for all tests) - ❌ WRONG:
sign-up-critical-path.spec.ts(separate file for specific test) - ❌ WRONG:
sign-up-validation.spec.ts(separate file for specific test)
Examples:
// ✅ Specific test request - create only this test
test("User can create account and login successfully",{
tag: ['@critical', '@e2e', '@signup', '@SIGNUP-E2E-001']
} async ({ page }) => {
// Implementation for this specific test only
});
// ❌ Don't create full suite when only one test is requested
Request Examples:
- "Create a test for user sign-up" → Create only the sign-up test, not the full suite
- "Generate E2E tests for the login page" → Create comprehensive test suite with all scenarios
- "Add a test to verify form validation" → Add only the validation test to existing spec
- "Create tests for the home page" → Create full test suite for home functionality
- "Create a new test e2e for sign-up" → Create only the specific test mentioned
- "Generate comprehensive E2E tests for sign-up" → Create full test suite
Key Phrases to Identify Scope:
- Single Test: "a test", "one test", "new test", "add test"
- Full Suite: "comprehensive tests", "all tests", "test suite", "complete tests", "generate tests"
Page Object Model Pattern
- Extend BasePage: All page objects should extend
BasePagefor common functionality - REUSE Existing Page Objects: Always check for existing page objects before creating new ones
- Interface Definitions: Define clear interfaces for form data and credentials
- Method Organization: Group methods by functionality (navigation, form interaction, validation, etc.)
- Locator Strategy: Use stable selectors (name attributes, labels) over fragile CSS selectors
- Avoid Code Duplication: When creating a new page object, verify if there are repeated methods across page objects that should be moved to
BasePage - Shared Utilities: If utility functions are repeated across tests, create or update
tests/helpers.tsto centralize them - Refactor to BasePage: Common patterns like form validation, notification checks, or navigation should be extracted to
BasePage - Refactor to Helpers: Data generation, test setup utilities, or common assertions should be extracted to
tests/helpers.ts
Page Object Reuse Guidelines
- Check existing page objects first: Look in
tests/directory for existing page objects - Import and reuse: Use existing page objects like
SignInPage,HomePage, etc. - Create page objects when needed: If a test requires interaction with a page that doesn't have a page object yet, create it following the Page Object Model pattern
- Only create new page objects when the page doesn't exist or has unique functionality
- Example: For a sign-up test that needs to verify login after signup, reuse
SignInPageandHomePageif they exist, or create them if needed - Avoid duplication: Don't recreate functionality that already exists in other page objects
- Complete dependencies: When creating a test that requires multiple page interactions, ensure all necessary page objects exist (create them if they don't)
Code Refactoring Guidelines
When to move code to BasePage:
- ✅ Navigation helpers used by multiple pages (e.g.,
waitForPageLoad(),getCurrentUrl()) - ✅ Common UI interactions (e.g., clicking notifications, handling modals, theme toggles)
- ✅ Verification patterns repeated across pages (e.g.,
isVisible(),waitForVisible()) - ✅ Error handling that applies to all pages
- ✅ Screenshot utilities for debugging
When to move code to tests/helpers.ts:
- ✅ Test data generation (e.g.,
generateUniqueEmail(),generateTestUser()) - ✅ Setup/teardown utilities (e.g.,
createTestUser(),cleanupTestData()) - ✅ Custom assertions used across tests (e.g.,
expectNotificationToContain()) - ✅ API helpers for test setup (e.g.,
seedDatabase(),resetState()) - ✅ Time utilities (e.g.,
waitForCondition(),retryAction())
Example - Before Refactoring:
// ❌ BAD: Repeated code in multiple page objects
export class SignUpPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
export class SignInPage extends BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
}
Example - After Refactoring:
// ✅ GOOD: Move to BasePage
export class BasePage {
async waitForNotification(): Promise<void> {
await this.page.waitForSelector('[role="status"]');
}
async verifyNotificationMessage(message: string): Promise<void> {
const notification = this.page.locator('[role="status"]');
await expect(notification).toContainText(message);
}
}
// ✅ GOOD: Move to helpers.ts for data generation
export function generateUniqueEmail(): string {
const timestamp = Date.now();
return `test.user.${timestamp}@example.com`;
}
export function generateTestUser() {
return {
name: "Test User",
email: generateUniqueEmail(),
password: "TestPassword123!",
};
}
Page Object Reuse Example:
// ✅ GOOD: Check for existing page objects, create if needed
// 1. Check if SignInPage exists - if not, create it
// 2. Check if HomePage exists - if not, create it
import { SignInPage } from "../sign-in/sign-in-page";
import { HomePage } from "../home/home-page";
test("User can sign up and login", async ({ page }) => {
const signUpPage = new SignUpPage(page);
const signInPage = new SignInPage(page); // REUSE existing (or create if missing)
const homePage = new HomePage(page); // REUSE existing (or create if missing)
// Use existing functionality
await signUpPage.signUp(userData);
await homePage.verifyPageLoaded(); // REUSE existing method
await homePage.signOut(); // REUSE existing method
await signInPage.login(credentials); // REUSE existing method
});
// ❌ BAD: Don't recreate existing functionality in SignUpPage
export class SignUpPage extends BasePage {
// Don't recreate logout functionality
async logout() {
/* ... */
} // ❌ HomePage already has this
// Don't recreate login functionality
async login() {
/* ... */
} // ❌ SignInPage already has this
// ✅ GOOD: Instead, use composition or delegation
async loginAfterSignUp(credentials: LoginCredentials): Promise<void> {
// Reuse SignInPage methods or delegate to it
const emailField = this.page.getByRole("textbox", { name: "Email*" });
const passwordField = this.page.getByRole("textbox", { name: "Password*" });
const loginButton = this.page.getByRole("button", { name: "Log in" });
await emailField.fill(credentials.email);
await passwordField.fill(credentials.password);
await loginButton.click();
}
}
Page Object Structure:
export interface FeatureData {
email: string;
password: string;
// ... other fields
}
export class FeaturePage extends BasePage {
// Form elements
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
super(page);
// Use stable selectors
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.getByRole("button", { name: "Submit" });
}
async goto(): Promise<void> {
await super.goto("/feature-path");
}
async performAction(data: FeatureData): Promise<void> {
await this.emailInput.fill(data.email);
await this.passwordInput.fill(data.password);
await this.submitButton.click();
}
async verifyCriticalOutcome(): Promise<void> {
await expect(this.page).toHaveURL("/expected-path");
// ... verification logic
}
}
Test Structure Best Practices
- Page Object Usage: Use Page Object Models for all page interactions
- Tag Organization: Use Playwright tag syntax for test categorization
- Test IDs: Include test case IDs in tags for traceability
- Verification Steps: Include clear verification steps for each major action
Key Elements:
- Page Objects: All interactions through Page Object Models
- Clear Tags: Use
{ tag: ['@priority', '@type', '@feature', '@test-id'] }syntax - Verification: Explicit verification of critical outcomes
Playwright Selector Best Practices
When creating locators in Page Objects, follow this priority order for maximum reliability:
✅ Primary Selectors (Recommended):
getByRole(): The best and most robust for all interactive elements (buttons, links, main sections)getByLabel(): The best for form controls that have an associated label
⚠️ Secondary Selectors (Use Sparingly):
getByText(): Use only when the above fail or for static text verification (headings, paragraphs, messages)- Others (e.g.
getByTestId()): Use only as a last resort when the above fail or are not applicable
Examples:
// ✅ GOOD - Using getByRole for interactive elements
this.submitButton = page.getByRole("button", { name: "Submit" });
this.navigationLink = page.getByRole("link", { name: "Dashboard" });
// ✅ GOOD - Using getByLabel for form controls
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
// ⚠️ SPARINGLY - Using getByText only when necessary
this.errorMessage = page.getByText("Invalid credentials"); // Only if no better selector exists
this.pageTitle = page.getByText("Welcome to Prowler"); // Only for static content verification
// ❌ AVOID - Using fragile selectors when better options exist
this.submitButton = page.locator(".btn-primary"); // Use getByRole instead
this.emailInput = page.locator("#email"); // Use getByLabel instead
Tag Syntax Example:
test(
"Test description",
{ tag: ["@critical", "@e2e", "@signup", "@SIGNUP-E2E-001"] },
async ({ page }) => {
// Test implementation
},
);
E2E Test Documentation Format
Each test documentation file ({page-name}.md) should follow this structured format:
### E2E Tests: {Feature Name}
**Suite ID:** `{SUITE-ID}`
**Feature:** {Feature description}
---
## Test Case: `{TEST-ID}` - {Test case title}
**Priority:** `{critical|high|medium|low}`
**Tags:**
- type → @e2e
- feature → @{feature-name}
**Description/Objective:** {Brief description of what the test validates}
**Preconditions:**
- {List of prerequisites for the test to run}
- {Any required data or state}
### Flow Steps:
1. {Step 1 description}
2. {Step 2 description}
3. {Step 3 description}
...
### Expected Result:
- {Expected outcome 1}
- {Expected outcome 2}
...
### Key verification points:
- {Key assertion 1}
- {Key assertion 2}
- {Key assertion 3}
### Notes:
- {Any additional notes or considerations}
- {Test data requirements or constraints}
Test Documentation Best Practices
- Suite ID Format: Use descriptive suite IDs (e.g.,
SIGNUP-E2E) - Test ID Format: Include feature and sequence (e.g.,
SIGNUP-E2E-001) - Priority Levels: Use
critical,high,medium,lowfor test prioritization - Tag Organization: Use Playwright tag syntax:
{ tag: ['@priority', '@type', '@feature', '@test-id'] } - Flow Steps: Number steps clearly and describe user actions
- Verification Points: List specific assertions and expected outcomes
- Preconditions: Document any required setup or data dependencies
- Test Data Notes: Include information about data generation and uniqueness strategies
Tag Categories:
- Priority:
@critical,@high,@medium,@low - Type:
@e2e - Feature:
@signup,@signin,@dashboard - Test ID:
@SIGNUP-E2E-001,@LOGIN-E2E-002
IMPORTANT - Keep Documentation Concise:
- ❌ DO NOT include general test running instructions
- ❌ DO NOT include file structure explanations
- ❌ DO NOT include code examples or tutorials
- ❌ DO NOT include extensive troubleshooting sections
- ❌ DO NOT include command references or configuration details
- ✅ DO focus only on the specific test case: flow, preconditions, expected results, and verification points
- ✅ DO keep the documentation under 60 lines when possible
- ✅ DO follow the exact format template provided above
Component Testing (Future)
- Jest + React Testing Library
- Component unit tests
- Integration tests for complex flows
Performance
- Keep Client Components lean; avoid heavy client-side logic where server boundary is possible
- Use streaming, partial rendering, and skeletons for long operations
- DO NOT use
useMemooruseCallback- React 19 Compiler handles this automatically - App Router with server components
- Image optimization with next/image
- Font optimization with next/font
- Bundle analysis and code splitting
Security
Best Practices
- Do not commit secrets or
.env.localvalues. Use placeholders in examples - Avoid logging sensitive data. Sanitize error messages shown to users
- Strict CSP headers in next.config.js
- XSS protection and CSRF mitigation
- Secure cookie configuration
Data Handling
- Client-side validation with Zod
- Server-side sanitization
- Secure credential storage patterns
Authentication
- NextAuth: use server helpers and
server-onlywhere appropriate - Protect routes through middleware and layout boundaries
- JWT tokens via NextAuth
- Automatic token refresh
- Protected routes via middleware
Quality Gates (before submitting changes) IMPORTANT!
npm run typecheckshows 0 new errorsnpm run lint:checkpasses or is fixed vianpm run lint:fixnpm run format:checkpasses or is corrected vianpm run format:write- Relevant Playwright specs pass locally
- UI states (loading, error, empty) are handled
Common Development Tasks
Adding New Pages
- Create page component in
app/(prowler)/ - Add route to navigation in
lib/menu-list.ts - Implement required server actions
- Add proper TypeScript types (using const-based type pattern). IMPORTANT!: avoid "any" type.
Creating Components
- Use shadcn/ui for new UI components that belong to new features/pages
- Existing features/pages should continue using HeroUI for consistency
- Follow established patterns in
components/shadcn/for new components - Implement proper TypeScript interfaces (using const-based type pattern)
- Add to component index files
Integrating with Backend
- Create server actions in
actions/ - Define TypeScript types in
types/(using const-based type pattern) - Handle loading and error states
- Implement proper caching strategy
When Implementing New UI
- Use shadcn/ui components with the new Tailwind theme for new UI features/pages
- For existing features/pages, continue using HeroUI components for consistency
- Start from existing patterns in the closest domain folder
- Reuse primitives from
components/shadcn(for new features) orcomponents/ui(HeroUI for existing) and existing composables - Add types to
types/if they're shared (2+ features); otherwise colocate types near usage (1 feature) - Follow The Scope Rule strictly - used by 2+ features = shared, 1 feature = local
- Update feature docs and
ui/CHANGELOG.mdwhen behavior changes
Environment Configuration
Required Environment Variables
# Authentication
NEXTAUTH_SECRET=your_secret_here
NEXTAUTH_URL=http://localhost:3000
# API Configuration
NEXT_PUBLIC_API_BASE_URL=http://localhost:8080
# AI Features
OPENAI_API_KEY=your_openai_key
Development Setup
- Copy
.env.exampleto.env.local - Configure authentication providers
- Set up API backend connection
- Install dependencies and start dev server
Deployment
Production Build
npm run build # Build optimized bundle
npm run start # Start production server
Docker Deployment
- Dockerfile available for containerization
- Standalone output for minimal container size
- Health checks via
/api/healthendpoint
Troubleshooting
Common Issues
- TypeScript errors: Run
npm run typecheck - Lint issues: Run
npm run lint:fix - Build failures: Check Next.js build logs
- Authentication issues: Verify NextAuth configuration
Debug Tools
- Next.js built-in debugger
- React Developer Tools
- Network tab for API debugging
- Lighthouse for performance analysis
Recent Major Migrations (January 2025)
- ✅ React 18 → 19.1.1 (async components, useActionState, React Compiler)
- ✅ Next.js 14 → 15.5.3 (enhanced App Router)
- ✅ NextUI → HeroUI 2.8.4
- ✅ Zod 3.25.73 → 4.1.11 (breaking: deprecated methods)
- ✅ Zustand 4.5.7 → 5.0.8 (compatible)
- ✅ AI SDK 4.3.16 → 5.0.59 (breaking: new message structure)
- ✅ LangChain updates with new adapter patterns
Summary
These guidelines ensure:
- ✅ Consistent code patterns across the project
- ✅ Optimal performance with Next.js 15 and React 19 with Compiler
- ✅ Clear, maintainable architecture following The Scope Rule
- ✅ Proper separation of concerns
- ✅ Type safety throughout the codebase using const-based types
- ✅ Semantic Tailwind usage without var() or hex colors in className
- ✅ Purposeful use of cn() only for conditionals and merging
When in doubt, ask before deviating from these patterns.
References
- High-level project guide:
../AGENTS.md(root Prowler project - takes priority) - UI Changelog:
./CHANGELOG.md