by thetestingacademy
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.
npx @qaskills/cli add protractor-testingAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
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.
waitForAngular(). Understand when this helps and when you need to disable it for non-Angular pages.by.model(), by.binding(), by.repeater(), and by.cssContainingText() for Angular/AngularJS-specific element targeting when they are available.browser.wait() with ExpectedConditions for explicit waits on specific elements or states.beforeEach rather than relying on other tests to create state.async/await in all new code.protractor.conf.js, element(by.model()), browser.get(), or browser.wait()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
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);
};
},
};
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();
}
}
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');
}
}
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');
});
});
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');
});
});
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);
});
});
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'
);
});
});
// 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();
}
}
async/await for clarity and future migration compatibility.data-testid selectors over Angular-specific locators (by.model, by.binding) for new tests. These translate directly to modern frameworks during migration.ExpectedConditions for explicit waits rather than browser.sleep(). Combine conditions with EC.and() and EC.or() for complex wait scenarios.chromeOptions.args with --headless. This reduces resource usage and speeds up pipeline execution.browser.waitForAngularEnabled(false). Forgetting this causes infinite waits on non-Angular content.allScriptsTimeout for page loads, defaultTimeoutInterval for individual tests, and specific timeouts for browser.wait() calls.directConnect: true to bypass Selenium Server and connect directly to ChromeDriver. This is faster and simpler for single-browser local testing.browser.sleep() -- Static waits slow tests and hide timing issues. Use browser.wait() with ExpectedConditions for reliable synchronization.async/await consistently.by.xpath() for simple queries -- XPath is slower and harder to read than CSS selectors. Only use XPath when CSS cannot express the query.shardTestFiles: true with maxInstances for parallelism.# 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
# 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
- name: Install QA Skills
run: npx @qaskills/cli add protractor-testing10 of 29 agents supported