seed-skills/protractor-testing/SKILL.md
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 skillsauth add PramodDutta/qaskills Protractor TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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('[email protected]', 'SecurePass123!');
const url = await browser.getCurrentUrl();
expect(url).toContain('/dashboard');
});
it('should show error for invalid credentials', async () => {
await loginPage.login('[email protected]', '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('[email protected]', '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('[email protected]');
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('[email protected]');
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
development
Build WebdriverIO E2E suites — wdio.conf.ts setup, $ and $$ selectors, auto-wait and waitUntil, Mocha framework structure, page objects, parallel capabilities, and services for visual testing and Appium mobile.
testing
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
testing
Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
development
Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.