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.
git clone https://github.com/PramodDutta/qaskills
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"
seed-skills/protractor-testing/SKILL.mdProtractor 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
- Angular-Aware Testing -- Protractor's key strength is automatic synchronization with Angular's change detection via
. Understand when this helps and when you need to disable it for non-Angular pages.waitForAngular() - Angular-Specific Locators -- Use
,by.model()
,by.binding()
, andby.repeater()
for Angular/AngularJS-specific element targeting when they are available.by.cssContainingText() - 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.
- Explicit over Implicit Waits -- While Protractor handles Angular synchronization, use
withbrowser.wait()
for explicit waits on specific elements or states.ExpectedConditions - 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.
- Test Data Independence -- Each test must set up its own data. Use API calls or database seeds in
rather than relying on other tests to create state.beforeEach - Control Flow Management -- Protractor uses WebDriver's control flow for promise management. Understand the async/await migration path and prefer explicit
in all new code.async/await
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())
, orbrowser.get()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
- Use async/await everywhere -- Protractor's implicit promise management (control flow) is deprecated. Write all tests with explicit
for clarity and future migration compatibility.async/await - Prefer
selectors over Angular-specific locators (data-testid
,by.model
) for new tests. These translate directly to modern frameworks during migration.by.binding - Use
for explicit waits rather thanExpectedConditions
. Combine conditions withbrowser.sleep()
andEC.and()
for complex wait scenarios.EC.or() - Implement the Page Object pattern for every page and reusable component. This isolates locator changes and makes migration to other frameworks straightforward.
- Capture screenshots on failure using Jasmine reporter hooks. Visual evidence of failures dramatically speeds up debugging.
- Run headless in CI by configuring
withchromeOptions.args
. This reduces resource usage and speeds up pipeline execution.--headless - Disable Angular sync for non-Angular pages with
. Forgetting this causes infinite waits on non-Angular content.browser.waitForAngularEnabled(false) - Set appropriate timeouts --
for page loads,allScriptsTimeout
for individual tests, and specific timeouts fordefaultTimeoutInterval
calls.browser.wait() - 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.
- Use
to bypass Selenium Server and connect directly to ChromeDriver. This is faster and simpler for single-browser local testing.directConnect: true
Anti-Patterns
- Using
-- Static waits slow tests and hide timing issues. Usebrowser.sleep()
withbrowser.wait()
for reliable synchronization.ExpectedConditions - Relying on control flow promises -- The implicit promise chain is deprecated and behaves unpredictably. Use
consistently.async/await - Not disabling Angular sync for non-Angular pages -- Protractor hangs indefinitely waiting for Angular on pages that do not use Angular.
- Hardcoding test data -- Embedding credentials, URLs, and IDs directly in specs makes tests environment-dependent and hard to maintain.
- Using
for simple queries -- XPath is slower and harder to read than CSS selectors. Only use XPath when CSS cannot express the query.by.xpath() - Writing tests that depend on other tests -- Tests that require prior tests to run create fragile, non-parallelizable suites.
- Ignoring deprecation warnings -- Protractor is end-of-life. Continuing to build large new test suites on it accumulates technical debt.
- Not using Page Objects -- Putting locators directly in test files leads to duplication and makes selector changes expensive.
- Forgetting to handle stale elements -- After page transitions or dynamic updates, element references may become stale. Re-query elements after state changes.
- Running tests sequentially in CI -- Not configuring parallel execution wastes pipeline time. Use
withshardTestFiles: true
for parallelism.maxInstances
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