> Meticulous records real user sessions from a web app and replays them against each commit to catch visual regressions.
>
> Full AI-readable docs index (complete project setup journey + every page as markdown): https://app.meticulous.ai/llms.txt

# Angular - Complete Setup Guide

Complete guide for setting up Meticulous with Angular applications.

---

## Overview

Angular is a comprehensive framework for building web applications. This guide covers:

- **Recorder installation** in `src/index.html`
- **CI/CD configuration** with static asset upload
- **Authentication handling**
- **Common patterns** and troubleshooting

**Prerequisites**:
- Angular application (version 12+)
- Basic familiarity with [Meticulous concepts](/docs/onboarding-guide)

---

## Quick Start

### Step 1: Install Recorder in src/index.html

Add the Meticulous recorder script to your `src/index.html` **before any other scripts**.

**File**: `src/index.html`

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Your App Name</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">

  <!-- Meticulous recorder - MUST be first script -->
  <!-- Replace YOUR_PROJECT_ID with your project ID from the dashboard -->
  <script
    data-project-id="YOUR_PROJECT_ID"
    src="https://snippet.meticulous.ai/v1/meticulous.js"
  ></script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
```

**Important**: The recorder must load before Angular bootstraps.

---

### Step 2: Configure GitHub Actions Workflow

Angular builds to static files, so we use the `upload-assets` action.

**File**: `.github/workflows/meticulous.yml`

```yaml
name: Meticulous Tests

on:
  push:
    branches: [main]
  pull_request: {}
  workflow_dispatch: {}

permissions:
  actions: write
  contents: read
  issues: write
  pull-requests: write
  statuses: read

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build app
        run: npm run build
        env:
          NODE_ENV: production

      - name: Upload and test
        uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
        with:
          api-token: ${{ secrets.METICULOUS_API_TOKEN }}
          app-directory: "dist/your-app-name"
          rewrites: |
            [
              { "source": "/(.*)", "destination": "/index.html" }
            ]
```

**Important**: Replace `your-app-name` with your actual app name from `angular.json`.

---

### Step 3: Add API Token Secret

1. Get your API token from [Meticulous dashboard](https://app.meticulous.ai) → Project Settings
2. Go to GitHub repo → Settings → Secrets and variables → Actions
3. Create secret named `METICULOUS_API_TOKEN` with your token

---

## Finding Your App Name

The build output directory depends on your app name in `angular.json`:

**File**: `angular.json`

```json
{
  "projects": {
    "my-angular-app": {
      "architect": {
        "build": {
          "options": {
            "outputPath": "dist/my-angular-app"
          }
        }
      }
    }
  }
}
```

Use `dist/my-angular-app` as the `app-directory` in your workflow.

---

## Common Patterns

### Pattern 1: Detect Test Mode

For a quick check, you can access `window.Meticulous` directly. For a cleaner, reusable approach, see [Pattern 4: Service for Test Detection](#pattern-4-service-for-test-detection) below.

**Component**:
```typescript
import { Component, OnInit } from '@angular/core'

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html'
})
export class DashboardComponent implements OnInit {
  isTest = false

  ngOnInit() {
    this.isTest = (window as any).Meticulous?.isRunningAsTest || false

    if (this.isTest) {
      // Skip animations, use test data, etc.
    }
  }
}
```

**Template**:
```html
<div *ngIf="isTest" class="test-indicator">
  Running as test
</div>
```

### Pattern 2: Bypass Authentication

**File**: `src/app/app.component.ts`

```typescript
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(private router: Router) {}

  ngOnInit() {
    // Mock authentication for tests
    if ((window as any).Meticulous?.isRunningAsTest) {
      localStorage.setItem('auth-token', 'test-token')
      localStorage.setItem('user', JSON.stringify({
        id: 'test-user',
        name: 'Test User',
        email: 'test@example.com'
      }))
    }
  }
}
```

### Pattern 3: Route Guards

Handle authentication in route guards:

**File**: `src/app/guards/auth.guard.ts`

```typescript
import { Injectable } from '@angular/core'
import { Router, CanActivate } from '@angular/router'
import { AuthService } from '../services/auth.service'

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canActivate(): boolean {
    // Skip auth check during tests
    if ((window as any).Meticulous?.isRunningAsTest) {
      return true
    }

    // Normal auth check
    if (this.authService.isAuthenticated()) {
      return true
    }

    this.router.navigate(['/login'])
    return false
  }
}
```

### Pattern 4: Service for Test Detection

Create a reusable service:

**File**: `src/app/services/meticulous.service.ts`

```typescript
import { Injectable } from '@angular/core'

interface MeticulousWindow extends Window {
  Meticulous?: {
    isRunningAsTest?: boolean
    recordCustomValues?: (values: Record<string, any>) => void
    getCustomValues?: () => Record<string, any> | undefined
  }
}

@Injectable({
  providedIn: 'root'
})
export class MeticulousService {
  get isRunningAsTest(): boolean {
    return (window as MeticulousWindow).Meticulous?.isRunningAsTest || false
  }

  recordCustomValues(values: Record<string, any>): void {
    (window as MeticulousWindow).Meticulous?.recordCustomValues?.(values)
  }

  getCustomValues(): Record<string, any> | undefined {
    return (window as MeticulousWindow).Meticulous?.getCustomValues?.()
  }
}
```

**Usage**:
```typescript
import { Component, OnInit } from '@angular/core'
import { MeticulousService } from './services/meticulous.service'

@Component({
  selector: 'app-my-component',
  templateUrl: './my-component.component.html'
})
export class MyComponent implements OnInit {
  constructor(private meticulous: MeticulousService) {}

  ngOnInit() {
    if (this.meticulous.isRunningAsTest) {
      // Test-specific logic
    }
  }
}
```

---

## Complete Example

### File Structure

```
your-app/
├── src/
│   ├── app/
│   │   ├── app.component.ts      # Root component
│   │   ├── app-routing.module.ts # Router configuration
│   │   ├── guards/
│   │   │   └── auth.guard.ts     # Auth guard
│   │   ├── services/
│   │   │   ├── auth.service.ts   # Auth service
│   │   │   └── meticulous.service.ts
│   │   └── components/
│   ├── index.html                # Recorder installation
│   └── main.ts                   # Bootstrap
├── angular.json                  # Angular configuration
├── .github/
│   └── workflows/
│       └── meticulous.yml        # CI/CD
└── package.json
```

### Example: Protected Component

**File**: `src/app/components/dashboard/dashboard.component.ts`

```typescript
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { MeticulousService } from '../../services/meticulous.service'

interface User {
  id: string
  name: string
  email: string
}

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
  user: User | null = null
  loading = true
  error = ''

  constructor(
    private router: Router,
    private meticulous: MeticulousService
  ) {}

  async ngOnInit() {
    // Mock data for tests
    if (this.meticulous.isRunningAsTest) {
      this.user = {
        id: 'test-user-123',
        name: 'Test User',
        email: 'test@example.com'
      }
      this.loading = false
      return
    }

    // Normal data fetching
    try {
      const response = await fetch('/api/user')
      if (!response.ok) throw new Error('Failed to fetch user')
      this.user = await response.json()
    } catch (err) {
      this.error = err instanceof Error ? err.message : 'Unknown error'
      this.router.navigate(['/login'])
    } finally {
      this.loading = false
    }
  }
}
```

**File**: `src/app/components/dashboard/dashboard.component.html`

```html
<div class="dashboard">
  <h1 *ngIf="user">Welcome, {{ user.name }}!</h1>

  <div *ngIf="loading">
    <p>Loading dashboard...</p>
  </div>

  <div *ngIf="error" class="error">
    <p>{{ error }}</p>
  </div>

  <div *ngIf="!loading && !error && user">
    <p>Email: {{ user.email }}</p>
    <!-- Dashboard content -->
  </div>
</div>
```

---

## CI/CD Configuration Details

### Environment Variables

Angular doesn't have built-in support for runtime environment variables in the browser. Use build-time configuration:

**File**: `src/environments/environment.prod.ts`

```typescript
export const environment = {
  production: true,
  apiUrl: 'https://api.example.com'
}
```

**In workflow**:
```yaml
- name: Build app
  run: npm run build -- --configuration production
```

### Custom Output Directory

If you have a custom output directory in `angular.json`:

```json
{
  "architect": {
    "build": {
      "options": {
        "outputPath": "build"
      }
    }
  }
}
```

Update workflow:
```yaml
- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "build"
```

---

## Troubleshooting

### Issue: Recorder Not Loading

**Symptom**: `window.Meticulous` is undefined

**Checks**:
1. Verify recorder script in `src/index.html` `<head>`
2. Check project ID is correct
3. Check for CSP blocking

**Fix**: Ensure correct placement:

```html
<head>
  <!-- Recorder FIRST -->
  <script data-project-id="..." src="https://snippet.meticulous.ai/v1/meticulous.js"></script>

  <!-- Then other elements -->
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
```

---

### Issue: TypeScript Errors

**Symptom**: `Property 'Meticulous' does not exist on type 'Window'.`

**Fix**: Add type declarations

**File**: `src/typings.d.ts`

```typescript
interface Meticulous {
  isRunningAsTest?: boolean
  recordCustomValues?: (values: Record<string, any>) => void
  getCustomValues?: () => Record<string, any> | undefined
  pause?: () => void
  resume?: () => void
}

declare interface Window {
  Meticulous?: Meticulous
}
```

**Update**: `tsconfig.app.json`

```json
{
  "files": [
    "src/main.ts",
    "src/typings.d.ts"
  ]
}
```

---

### Issue: Wrong Output Directory

**Symptom**: `app-directory "dist/your-app-name" not found`

**Cause**: App name doesn't match `angular.json`

**Fix**: Check `angular.json` for correct output path:

```bash
# Find your app name
cat angular.json | grep "outputPath"

# Output example: "outputPath": "dist/my-app"
# Use "dist/my-app" in workflow
```

---

### Issue: Routes Return 404

**Symptom**: Direct navigation to routes returns 404

**Cause**: Missing rewrite configuration

**Fix**:

```yaml
rewrites: |
  [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
```

---

### Issue: False Positive Diffs

**Common causes**:
1. Animations not completing
2. Random data
3. Timestamps

**Fixes**:

**Disable animations in tests**:
```typescript
import { Component, OnInit } from '@angular/core'
import { MeticulousService } from './services/meticulous.service'

@Component({
  selector: 'app-my-component',
  animations: [/* your animations */]
})
export class MyComponent implements OnInit {
  animationState = 'initial'

  constructor(private meticulous: MeticulousService) {}

  ngOnInit() {
    // Skip animations in tests
    if (this.meticulous.isRunningAsTest) {
      this.animationState = 'final'
    }
  }
}
```

**Deterministic values**:
```typescript
generateId(): string {
  if ((window as any).Meticulous?.isRunningAsTest) {
    return 'test-id-12345'
  }
  return crypto.randomUUID()
}
```

**Ignore timestamps**:
```html
<span class="meticulous-ignore">
  {{ currentDate | date:'medium' }}
</span>
```

Learn more: [Fix False Positive Diffs](/docs/how-to/fix-false-positive-diffs)

---

## Testing Best Practices

### 1. Handle Loading States

Always show loading states:

```html
<div *ngIf="loading">
  <app-skeleton-loader></app-skeleton-loader>
</div>
<div *ngIf="!loading && !error">
  <!-- Content -->
</div>
<div *ngIf="error">
  <app-error-message [error]="error"></app-error-message>
</div>
```

### 2. Use Resolvers for Data

Angular resolvers ensure data is loaded before navigation:

```typescript
import { Injectable } from '@angular/core'
import { Resolve } from '@angular/router'
import { Observable } from 'rxjs'

@Injectable({
  providedIn: 'root'
})
export class UserResolver implements Resolve<User> {
  constructor(
    private userService: UserService,
    private meticulous: MeticulousService
  ) {}

  resolve(): Observable<User> | Promise<User> | User {
    if (this.meticulous.isRunningAsTest) {
      return {
        id: 'test-user',
        name: 'Test User',
        email: 'test@example.com'
      }
    }

    return this.userService.getUser()
  }
}
```

### 3. Test Locally First

```bash
# Build your app
npm run build

# Serve built files
npx http-server dist/your-app-name

# Verify at http://localhost:8080
```

---

## Advanced Configuration

### Monorepo Setup

```yaml
- name: Install dependencies
  working-directory: ./apps/frontend
  run: npm ci

- name: Build app
  working-directory: ./apps/frontend
  run: npm run build

- name: Upload and test
  uses: alwaysmeticulous/report-diffs-action/upload-assets@v1
  with:
    api-token: ${{ secrets.METICULOUS_API_TOKEN }}
    app-directory: "./apps/frontend/dist/frontend"
    rewrites: |
      [
        { "source": "/(.*)", "destination": "/index.html" }
      ]
```

### Base Href Configuration

If your app is served from a subdirectory:

**In `angular.json`**:
```json
{
  "build": {
    "options": {
      "baseHref": "/my-app/"
    }
  }
}
```

**In workflow**:
```yaml
rewrites: |
  [
    { "source": "/my-app/(.*)", "destination": "/index.html" }
  ]
```

---

## See Also

- [Onboarding Guide](/docs/onboarding-guide) - General Meticulous setup
- [Troubleshoot Authentication](/docs/how-to/troubleshoot-auth) - Auth patterns and solutions
- [Fix False Positives](/docs/how-to/fix-false-positive-diffs) - Handle non-deterministic content
