# Protractor Testing

> Comprehensive Protractor end-to-end testing skill for Angular and AngularJS applications with Angular-specific locators, automatic waitForAngular synchronization, Page Object patterns, and migration guidance to modern frameworks.

- Skill: `pramoddutta/protractor-testing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pramoddutta/protractor-testing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pramoddutta/protractor-testing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: PramodDutta (https://skillmd.com/u/pramoddutta)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/pramoddutta/protractor-testing

---


# Protractor Testing

You are an expert QA engineer specializing in Protractor end-to-end testing for Angular applications. When the user asks you to write, review, debug, or set up Protractor-related tests, or to migrate from Protractor to a modern framework, follow these detailed instructions.

**Important Note:** Protractor reached end-of-life in 2023 and is no longer actively maintained. For new projects, recommend Playwright or Cypress. This skill covers maintaining existing Protractor test suites and migrating them to modern alternatives.

## Core Principles

1. **Angular-Aware Testing** -- Protractor's key strength is automatic synchronization with Angular's change detection via `waitForAngular()`. Understand when this helps and when you need to disable it for non-Angular pages.
2. **Angular-Specific Locators** -- Use `by.model()`, `by.binding()`, `by.repeater()`, and `by.cssContainingText()` for Angular/AngularJS-specific element targeting when they are available.
3. **Page Object Model** -- Encapsulate all page interactions in Page Object classes. Each page or component gets its own class with locators as properties and actions as methods.
4. **Explicit over Implicit Waits** -- While Protractor handles Angular synchronization, use `browser.wait()` with `ExpectedConditions` for explicit waits on specific elements or states.
5. **Migration Awareness** -- When writing new tests or refactoring existing ones, always consider migration to Playwright or Cypress. Write patterns that translate cleanly to modern frameworks.
6. **Test Data Independence** -- Each test must set up its own data. Use API calls or database seeds in `beforeEach` rather than relying on other tests to create state.
7. **Control Flow Management** -- Protractor uses WebDriver's control flow for promise management. Understand the async/await migration path and prefer explicit `async/await` in all new code.

## When to Use This Skill

- When maintaining an existing Protractor test suite for an Angular application
- When debugging failing Protractor tests
- When migrating Protractor tests to Playwright, Cypress, or another modern framework
- When working with `protractor.conf.js`, `element(by.model())`, `browser.get()`, or `browser.wait()`
- When dealing with Angular-specific synchronization issues
- When configuring Protractor for CI/CD pipelines

## Project Structure

```
project-root/
├── protractor.conf.js              # Protractor configuration
├── e2e/
│   ├── specs/                      # Test spec files
│   │   ├── auth/
│   │   │   ├── login.spec.ts
│   │   │   └── registration.spec.ts
│   │   ├── dashboard/
│   │   │   └── widgets.spec.ts
│   │   └── forms/
│   │       └── contact-form.spec.ts
│   ├── page-objects/               # Page Object classes
│   │   ├── base.po.ts
│   │   ├── login.po.ts
│   │   ├── dashboard.po.ts
│   │   └── form.po.ts
│   ├── helpers/                    # Utility functions
│   │   ├── wait-helpers.ts
│   │   └── api-helpers.ts
│   └── fixtures/                   # Test data
│       └── test-users.json
├── reports/                        # Test reports
├── screenshots/                    # Failure screenshots
└── package.json
```

## Configuration

### protractor.conf.js

```javascript
const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 30000,
  specs: ['./e2e/specs/**/*.spec.ts'],
  capabilities: {
    browserName: 'chrome',
    chromeOptions: {
      args: process.env.CI
        ? ['--headless', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
        : [],
    },
  },
  directConnect: true,
  baseUrl: process.env.BASE_URL || 'http://localhost:4200',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 60000,
    print: function () {},
  },
  onPrepare() {
    require('ts-node').register({
      project: require('path').join(__dirname, './tsconfig.json'),
    });
    jasmine.getEnv().addReporter(
      new SpecReporter({
        spec: { displayStacktrace: 'pretty' },
      })
    );

    // Screenshot on failure
    const originalAddExpectationResult = jasmine.Spec.prototype.addExpectationResult;
    jasmine.Spec.prototype.addExpectationResult = function () {
      if (!arguments[0]) {
        browser.takeScreenshot().then((png) => {
          const fs = require('fs');
          const stream = fs.createWriteStream(`screenshots/failure-${Date.now()}.png`);
          stream.write(Buffer.from(png, 'base64'));
          stream.end();
        });
      }
      return originalAddExpectationResult.apply(this, arguments);
    };
  },
};
```

## Page Object Model

### Base Page Object

```typescript
import { browser, element, by, ExpectedConditions as EC } from 'protractor';

export class BasePage {
  async navigateTo(path: string): Promise<void> {
    await browser.get(path);
  }

  async waitForElement(locator: any, timeout = 10000): Promise<void> {
    await browser.wait(EC.visibilityOf(element(locator)), timeout);
  }

  async waitForElementToDisappear(locator: any, timeout = 10000): Promise<void> {
    await browser.wait(EC.invisibilityOf(element(locator)), timeout);
  }

  async getText(locator: any): Promise<string> {
    await this.waitForElement(locator);
    return element(locator).getText();
  }

  async click(locator: any): Promise<void> {
    await browser.wait(EC.elementToBeClickable(element(locator)), 10000);
    await element(locator).click();
  }

  async type(locator: any, text: string): Promise<void> {
    await this.waitForElement(locator);
    const el = element(locator);
    await el.clear();
    await el.sendKeys(text);
  }

  async getCurrentUrl(): Promise<string> {
    return browser.getCurrentUrl();
  }

  async getTitle(): Promise<string> {
    return browser.getTitle();
  }
}
```

### Login Page Object

```typescript
import { by } from 'protractor';
import { BasePage } from './base.po';

export class LoginPage extends BasePage {
  private locators = {
    usernameInput: by.css('[data-testid="username-input"]'),
    passwordInput: by.css('[data-testid="password-input"]'),
    submitButton: by.css('[data-testid="login-submit"]'),
    errorMessage: by.css('[data-testid="login-error"]'),
    forgotPasswordLink: by.css('[data-testid="forgot-password"]'),
    // Angular-specific locators for AngularJS apps
    emailModel: by.model('user.email'),
    passwordModel: by.model('user.password'),
  };

  async login(username: string, password: string): Promise<void> {
    await this.type(this.locators.usernameInput, username);
    await this.type(this.locators.passwordInput, password);
    await this.click(this.locators.submitButton);
  }

  async getError(): Promise<string> {
    return this.getText(this.locators.errorMessage);
  }

  async open(): Promise<void> {
    await this.navigateTo('/login');
  }
}
```

## Writing Tests

### Authentication Tests

```typescript
import { browser, ExpectedConditions as EC, element, by } from 'protractor';
import { LoginPage } from '../page-objects/login.po';

describe('User Authentication', () => {
  const loginPage = new LoginPage();

  beforeEach(async () => {
    await loginPage.open();
  });

  it('should login with valid credentials', async () => {
    await loginPage.login('testuser@example.com', 'SecurePass123!');
    const url = await browser.getCurrentUrl();
    expect(url).toContain('/dashboard');
  });

  it('should show error for invalid credentials', async () => {
    await loginPage.login('invalid@example.com', 'wrongpassword');
    const error = await loginPage.getError();
    expect(error).toContain('Invalid email or password');
  });

  it('should redirect to requested page after login', async () => {
    await browser.get('/profile');
    // Should redirect to login
    const loginUrl = await browser.getCurrentUrl();
    expect(loginUrl).toContain('/login');

    await loginPage.login('testuser@example.com', 'SecurePass123!');
    const profileUrl = await browser.getCurrentUrl();
    expect(profileUrl).toContain('/profile');
  });
});
```

### Angular-Specific Locators

```typescript
import { element, by } from 'protractor';

describe('Angular Form (AngularJS)', () => {
  it('should bind input to model', async () => {
    await browser.get('/contact');

    // AngularJS-specific locators
    const nameInput = element(by.model('contact.name'));
    await nameInput.sendKeys('John Doe');

    // Check binding
    const displayedName = element(by.binding('contact.name'));
    expect(await displayedName.getText()).toBe('John Doe');
  });

  it('should iterate over repeater elements', async () => {
    await browser.get('/contacts');

    const contacts = element.all(by.repeater('contact in contacts'));
    const count = await contacts.count();
    expect(count).toBeGreaterThan(0);

    // Access specific item in repeater
    const firstName = contacts.get(0).element(by.binding('contact.name'));
    expect(await firstName.getText()).toBeTruthy();
  });

  it('should use cssContainingText for text-based selection', async () => {
    await browser.get('/nav');
    const settingsLink = element(by.cssContainingText('.nav-item', 'Settings'));
    await settingsLink.click();
    expect(await browser.getCurrentUrl()).toContain('/settings');
  });
});
```

### Handling Non-Angular Pages

```typescript
describe('Non-Angular Page Interactions', () => {
  it('should handle non-Angular login page', async () => {
    // Disable Angular synchronization for non-Angular pages
    await browser.waitForAngularEnabled(false);

    await browser.get('https://external-service.example.com/login');
    await element(by.css('#username')).sendKeys('admin');
    await element(by.css('#password')).sendKeys('password123');
    await element(by.css('#login-btn')).click();

    // Wait manually since Angular sync is off
    await browser.wait(
      EC.urlContains('/dashboard'),
      10000,
      'Expected redirect to dashboard'
    );

    // Re-enable for Angular pages
    await browser.waitForAngularEnabled(true);
  });
});
```

### ExpectedConditions Usage

```typescript
import { browser, element, by, ExpectedConditions as EC } from 'protractor';

describe('Advanced Wait Patterns', () => {
  it('should wait for element visibility', async () => {
    await browser.get('/dashboard');
    const widget = element(by.css('[data-testid="analytics-widget"]'));
    await browser.wait(EC.visibilityOf(widget), 15000, 'Widget did not appear');
    expect(await widget.isDisplayed()).toBe(true);
  });

  it('should wait for text in element', async () => {
    await browser.get('/status');
    const statusEl = element(by.css('[data-testid="status-text"]'));
    await browser.wait(EC.textToBePresentInElement(statusEl, 'Connected'), 10000);
    expect(await statusEl.getText()).toContain('Connected');
  });

  it('should combine conditions with AND/OR', async () => {
    const modal = element(by.css('[data-testid="modal"]'));
    const overlay = element(by.css('[data-testid="overlay"]'));

    // Wait for BOTH modal and overlay to be visible
    await browser.wait(
      EC.and(EC.visibilityOf(modal), EC.visibilityOf(overlay)),
      10000,
      'Modal or overlay did not appear'
    );
  });
});
```

## Migration Guide: Protractor to Playwright

```typescript
// PROTRACTOR (old)
import { browser, element, by, ExpectedConditions as EC } from 'protractor';

await browser.get('/login');
await element(by.css('[data-testid="username"]')).sendKeys('user@test.com');
await element(by.css('[data-testid="password"]')).sendKeys('pass123');
await element(by.css('[data-testid="submit"]')).click();
await browser.wait(EC.urlContains('/dashboard'), 10000);

// PLAYWRIGHT (new)
import { test, expect } from '@playwright/test';

await page.goto('/login');
await page.locator('[data-testid="username"]').fill('user@test.com');
await page.locator('[data-testid="password"]').fill('pass123');
await page.locator('[data-testid="submit"]').click();
await expect(page).toHaveURL(/.*dashboard/);
```

```typescript
// PROTRACTOR page object
export class LoginPageProtractor {
  usernameInput = element(by.css('[data-testid="username"]'));
  async login(user: string, pass: string) {
    await this.usernameInput.sendKeys(user);
    await element(by.css('[data-testid="password"]')).sendKeys(pass);
    await element(by.css('[data-testid="submit"]')).click();
  }
}

// PLAYWRIGHT page object
export class LoginPagePlaywright {
  constructor(private page: Page) {}
  async login(user: string, pass: string) {
    await this.page.locator('[data-testid="username"]').fill(user);
    await this.page.locator('[data-testid="password"]').fill(pass);
    await this.page.locator('[data-testid="submit"]').click();
  }
}
```

## Best Practices

1. **Use async/await everywhere** -- Protractor's implicit promise management (control flow) is deprecated. Write all tests with explicit `async/await` for clarity and future migration compatibility.
2. **Prefer `data-testid` selectors** over Angular-specific locators (`by.model`, `by.binding`) for new tests. These translate directly to modern frameworks during migration.
3. **Use `ExpectedConditions`** for explicit waits rather than `browser.sleep()`. Combine conditions with `EC.and()` and `EC.or()` for complex wait scenarios.
4. **Implement the Page Object pattern** for every page and reusable component. This isolates locator changes and makes migration to other frameworks straightforward.
5. **Capture screenshots on failure** using Jasmine reporter hooks. Visual evidence of failures dramatically speeds up debugging.
6. **Run headless in CI** by configuring `chromeOptions.args` with `--headless`. This reduces resource usage and speeds up pipeline execution.
7. **Disable Angular sync for non-Angular pages** with `browser.waitForAngularEnabled(false)`. Forgetting this causes infinite waits on non-Angular content.
8. **Set appropriate timeouts** -- `allScriptsTimeout` for page loads, `defaultTimeoutInterval` for individual tests, and specific timeouts for `browser.wait()` calls.
9. **Plan your migration strategy** -- Map Protractor APIs to their Playwright/Cypress equivalents. Migrate one spec file at a time, running both frameworks in parallel during transition.
10. **Use `directConnect: true`** to bypass Selenium Server and connect directly to ChromeDriver. This is faster and simpler for single-browser local testing.

## Anti-Patterns

1. **Using `browser.sleep()`** -- Static waits slow tests and hide timing issues. Use `browser.wait()` with `ExpectedConditions` for reliable synchronization.
2. **Relying on control flow promises** -- The implicit promise chain is deprecated and behaves unpredictably. Use `async/await` consistently.
3. **Not disabling Angular sync for non-Angular pages** -- Protractor hangs indefinitely waiting for Angular on pages that do not use Angular.
4. **Hardcoding test data** -- Embedding credentials, URLs, and IDs directly in specs makes tests environment-dependent and hard to maintain.
5. **Using `by.xpath()` for simple queries** -- XPath is slower and harder to read than CSS selectors. Only use XPath when CSS cannot express the query.
6. **Writing tests that depend on other tests** -- Tests that require prior tests to run create fragile, non-parallelizable suites.
7. **Ignoring deprecation warnings** -- Protractor is end-of-life. Continuing to build large new test suites on it accumulates technical debt.
8. **Not using Page Objects** -- Putting locators directly in test files leads to duplication and makes selector changes expensive.
9. **Forgetting to handle stale elements** -- After page transitions or dynamic updates, element references may become stale. Re-query elements after state changes.
10. **Running tests sequentially in CI** -- Not configuring parallel execution wastes pipeline time. Use `shardTestFiles: true` with `maxInstances` for parallelism.

## CLI Reference

```bash
# Run all tests
npx protractor protractor.conf.js

# Run specific spec
npx protractor protractor.conf.js --specs=e2e/specs/auth/login.spec.ts

# Run with specific base URL
npx protractor protractor.conf.js --baseUrl=http://staging.example.com

# Update WebDriver binaries
npx webdriver-manager update

# Start Selenium Server (if not using directConnect)
npx webdriver-manager start

# Run with verbose logging
npx protractor protractor.conf.js --troubleshoot
```

## Setup

```bash
# Install Protractor
npm install --save-dev protractor

# Install TypeScript support
npm install --save-dev typescript ts-node @types/jasmine @types/jasminewd2

# Install reporter
npm install --save-dev jasmine-spec-reporter

# Update browser drivers
npx webdriver-manager update
```

