Protractor's waitForAngularEnabled: When to Disable Angular Synchronization and How to Do It Safely
Protractor waits for Angular's $http and $timeout queues before every command — great until a test hits a non-Angular page and hangs. Here is how to toggle waitForAngularEnabled safely in a hybrid app.
14 Feb 2026, 05:58 UTC

The short answer
Protractor's biggest feature is automatic synchronization: before each command, it waits for Angular's $http and $timeout queues to drain, so you rarely need sleep() calls. But that same mechanism hangs forever on pages where Angular is not bootstrapped. The practical decision is this: keep synchronization on for Angular views, and toggle it off with browser.waitForAngularEnabled(false) the moment a test navigates to a non-Angular page, third-party iframe, or hybrid section — then turn it back on when you return.
Note up front: Protractor is deprecated and no longer actively maintained. If you are writing this configuration, you are likely keeping a legacy suite alive or planning a migration. Everything below reflects well-established behavior in the final Protractor releases (5.x), but verify against your bundled Selenium WebDriver version.
How the synchronization actually works
Protractor injects a script that calls angular.element(document.body).injector().get('$browser') and asks Angular's testability API whether any pending HTTP requests or timeouts remain. Commands block until the answer is "stable." Two consequences follow:
- If Angular never bootstrapped on the page, there is no testability API to query. Protractor waits for a response that never comes and the spec dies with an Angular stability timeout.
- Anything asynchronous that bypasses Angular — a
setTimeoutoutside$timeout, a WebSocket, a third-party widget's own XHR — is invisible to the wait. Synchronization does not cover it even on Angular pages.
A worked configuration for a hybrid app
Assume an app where /app is Angular but /docs is a static site and checkout embeds a third-party payment iframe. The config sets a global policy, and the spec toggles synchronization at navigation boundaries.
// protractor.conf.js
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['specs/**/*.spec.js'],
capabilities: {
browserName: 'chrome'
},
onPrepare: function () {
// Default policy: assume Angular unless a spec says otherwise.
browser.waitForAngularEnabled(true);
}
};
// specs/hybrid.spec.js
describe('hybrid app', function () {
var EC = protractor.ExpectedConditions;
it('handles Angular and non-Angular sections', function () {
// Angular section: synchronization is on, no sleeps needed.
browser.get('/app/dashboard');
element(by.css('.widget')).click();
// Cross the boundary: disable BEFORE navigating away.
browser.waitForAngularEnabled(false);
browser.get('/docs');
// No automatic stability now — wait explicitly.
var heading = element(by.css('h1'));
browser.wait(EC.visibilityOf(heading), 10000);
expect(heading.getText()).toContain('Documentation');
// Re-enable BEFORE returning to Angular.
browser.waitForAngularEnabled(true);
browser.get('/app/settings');
expect(element(by.css('.settings-form')).isPresent()).toBe(true);
});
});
The ordering matters: toggle synchronization before the browser.get() that crosses the boundary, not after. If you navigate first with synchronization still on, the get() itself will hang waiting for Angular on a page that does not have it.
Limits and common mistakes
Silent failure on non-Angular pages. With synchronization on, a non-Angular page does not produce a clear "no Angular here" error in all versions — you often just get a timeout. If a spec hangs at a browser.get(), check whether the target page actually bootstraps Angular (look for ng-app or an ng-version attribute in the served HTML).
Mixing implicit waits with synchronization. Setting manage().timeouts().implicitlyWait(...) on top of Protractor's control flow causes stacked, unpredictable delays. Pick one strategy: Protractor's synchronization plus explicit browser.wait() calls.
Forgetting to re-enable. If a spec disables synchronization and a later spec assumes the default, behavior depends on execution order. Re-enable in the same spec that disabled it, or reset it in an afterEach.
Assuming full coverage when disabled. With waitForAngularEnabled(false), every stability guarantee is gone. Any assertion that touches the DOM needs an explicit ExpectedConditions wait, or the test becomes timing-dependent and flaky.
How to verify the policy is working
Run the suite and check three things: the Angular section executes clicks and assertions with no manual sleeps; the non-Angular section does not produce Angular stability timeout messages in the log; and disabling then re-enabling synchronization across a navigation does not leave later specs hanging. A quick diagnostic is to add a temporary log line after each toggle (console.log('sync:', browser.waitForAngularEnabled) prints the function reference, so instead log your own flag) or simply observe which sections pause before commands. If you see "Timed out waiting for asynchronous Angular tasks" on a page you know is not Angular, the toggle is missing or misplaced.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.