Qaskills 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.

install
source · Clone the upstream repo
git clone https://github.com/PramodDutta/qaskills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/PramodDutta/qaskills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/seed-skills/protractor-testing" ~/.claude/skills/pramoddutta-qaskills-protractor-testing && rm -rf "$T"
manifest: seed-skills/protractor-testing/SKILL.md
source content

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

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

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

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

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

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

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

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

// 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/);
// 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

# 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

# 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