How to Test a Single-Page AngularJS Application
Test a legacy AngularJS single-page app with a practical pyramid: fast logic tests, focused integration checks, and a few browser-level user journeys.
Test an AngularJS single-page application at three levels: run fast unit tests for pure logic and isolated services, use focused integration tests for AngularJS wiring and rendered behavior, then cover a small number of critical user journeys in a real browser. Start by identifying the app’s existing toolchain and run its existing test command. AngularJS support ended in January 2022, and Protractor reached end of life in August 2023, so treat this as maintenance guidance for an existing application rather than a recommendation to start a new AngularJS project. AngularJS documentation states that support has ended; the Protractor project recommends migrating to another end-to-end solution.
1. Identify the app and its existing test setup
AngularJS (often called Angular 1) is distinct from modern Angular. Before changing dependencies or copying a command from a guide, inspect the repository and verify its versions, scripts, test configuration, and supported browsers. Historical AngularJS project guidance documented Jasmine and Karma for unit and integration tests and Protractor for end-to-end tests. That describes a historical convention; Protractor is no longer a good choice for a new test suite.
- Check
package.jsonand the lockfile for the installed AngularJS version, test runner, assertion library, browser automation, and build tools. - Inspect scripts in
package.json, along with files such askarma.conf.js,protractor.conf.js, and any CI workflow files that are present. - Run the test command already defined by the project. Record whether it watches files, exits after one run, needs a browser, or depends on a local server.
- Check the browsers the application must support and the versions available in local development and CI.
- Separate existing failures from new changes before adding coverage. A legacy app may need its original Node.js version or browser before its existing setup can run.
Do not install a guessed version of Jasmine, Karma, AngularJS mocks, or a browser driver based on a current Angular tutorial. The correct setup depends on the repository’s actual dependency versions. Modern Angular’s test instructions are for a different framework; its current defaults are not a drop-in AngularJS recipe.
2. Build a test pyramid around user-visible risk
| Level | What to verify | Typical scope | When it helps |
|---|---|---|---|
| Unit | Inputs, outputs, branching, validation, formatting, and isolated service logic | One function or service behavior | Business rules and edge cases should be quick to run and diagnose. |
| Integration | AngularJS dependency wiring, directive or component behavior, templates, and interaction between collaborators | A small piece of the app | The behavior depends on AngularJS wiring or rendered markup, but does not require a full user journey. |
| End to end | A few important flows through the running app in a real browser | Route, controls, and visible result | Catch failures that only emerge when the app is built, served, routed, and used as a whole. |
This division is a practical synthesis of the AngularJS project’s historical Jasmine/Karma and Protractor conventions and the Protractor tutorial’s browser interaction examples. It is not a requirement that every application use those exact tools or have a fixed test ratio. Put most cases at the fast levels; use browser tests for high-impact paths where the extra setup pays for itself.
3. Write fast tests for logic and services
Keep unit tests independent of the DOM and external services wherever possible. A small pure function is a useful example of the test shape. This Jasmine spec is runnable when Jasmine is already available in the project; it does not prescribe installing a particular version.
// price.js
function totalWithTax(subtotal, taxRate) {
if (!Number.isFinite(subtotal) || subtotal < 0) {
throw new TypeError('subtotal must be a non-negative number');
}
if (!Number.isFinite(taxRate) || taxRate < 0) {
throw new TypeError('taxRate must be a non-negative number');
}
return Math.round(subtotal * (1 + taxRate) * 100) / 100;
}
// price.spec.js — run with the project's Jasmine setup
describe('totalWithTax', function () {
it('adds tax and rounds to cents', function () {
expect(totalWithTax(10, 0.0825)).toBe(10.83);
});
it('handles a zero tax rate', function () {
expect(totalWithTax(12, 0)).toBe(12);
});
it('rejects invalid values', function () {
expect(function () { totalWithTax(-1, 0.1); }).toThrowError(TypeError);
expect(function () { totalWithTax(10, NaN); }).toThrowError(TypeError);
});
});
For an AngularJS service, test the contract that matters to callers: values returned, errors surfaced, and important branches. If a service issues HTTP requests, control the response rather than depending on a live API. The exact AngularJS mocking module and APIs vary with the application’s installed version and test setup. Look them up in documentation matching that version before adding code; do not copy modern Angular’s HttpTestingController into an AngularJS project.
4. Add focused integration tests for AngularJS behavior
Use an integration test when the behavior depends on AngularJS assembling pieces together: for example, a form directive bound to a model, a component rendering a service result, or a route view showing the right state. Keep the assertions about meaningful behavior: what appears, what a control allows, and what happens after an interaction.
- Include the actual template or a small representative template when template behavior is the point of the test.
- Exercise the public behavior exposed by the directive, component, or view instead of reaching into private scope variables.
- Give network calls, timers, and other asynchronous dependencies deterministic outcomes.
- Check an error or empty state as well as the successful state when users need to recover from it.
The exact test bootstrap, module loading, compilation, and HTTP-mocking APIs depend on the AngularJS version and project tooling. Verify those APIs against the app’s matching documentation and existing tests before writing version-specific integration code. This avoids a plausible-looking example that silently assumes a different AngularJS generation or bundler.
5. Cover a critical journey in a real browser
A browser-level test should follow a user path: open a route, enter valid information, submit, and assert the visible success state. Prefer stable selectors tied to meaningful controls or accessible labels. Avoid selectors based on incidental layout or generated class names. The archived Protractor tutorial demonstrates the historical shape of this test—navigation, locators, clicks, and a rendered text assertion—but Protractor itself is end of life.
// Browser-test pseudocode: adapt to the actively maintained runner
// selected for this repository and its supported browsers.
test('a user can submit a valid request', async function () {
await page.goto('/request');
await page.getByLabel('Email').fill('dev@example.test');
await page.getByRole('button', { name: 'Submit request' }).click();
await expect(page.getByRole('status'))
.toHaveText('Your request was received');
});
This is intentionally runner-neutral rather than a copy-paste test for a specific browser framework: the dossier does not establish which current runner or setup is compatible with every legacy AngularJS app. Replace page and the test helpers with the chosen runner’s APIs, and verify that runner against the app’s browser and CI requirements. The assertion should describe something a user can observe, not an AngularJS scope value or private framework detail.
For an existing Protractor suite, keep its tests running only as part of a deliberate maintenance plan. The project reached end of life in August 2023 and discourages new adoption. Evaluate replacement options against browser and operating-system coverage, compatibility with the legacy app, selector strategy, CI support, debugging workflow, team familiarity, and maintenance status. Playwright, WebdriverIO, or another tool may be candidates to evaluate, but no one choice is automatically compatible with every AngularJS app.
6. Make asynchronous work and dependencies predictable
Flaky tests often depend on uncontrolled timing or external systems. Make the result of each asynchronous step explicit and reproducible.
- Network: return controlled success, empty, slow, and failure responses for the cases the UI must handle. Avoid tests that depend on a third-party service being available.
- Timing: wait for a visible state or a specific condition rather than sleeping for an arbitrary duration. Use a fixed delay only when testing delay behavior itself.
- Routing: exercise the route through the application for end-to-end coverage; keep route setup and view wiring focused in integration coverage.
- Cleanup: restore spies, mock responses, timers, and browser state after each case so one test does not affect another.
- Failure states: assert what the user sees if a request fails or returns no data, not just that the promise or callback settled.
AngularJS provides its own generation-specific testing conventions. Confirm the app’s installed version and use its matching documentation for HTTP, route, directive, and component test APIs. Do not transplant modern Angular testing patterns into AngularJS without checking compatibility.
7. Run the suite in CI and use coverage carefully
Use the project’s existing test command in CI first, then make the runner exit with a useful failure status and run in the intended browser environment. Pin or otherwise consistently provision the browser and runtime that the project supports. A passing local run is not enough if CI uses a different browser, Node.js version, environment variables, or server configuration.
- Run the same command locally that CI will run, using the checked-in lockfile.
- Make the app server start predictably and wait until it is ready before browser tests begin.
- Keep fast unit and integration tests available as a separate job or command if the repository already supports that split.
- Capture failure output that identifies the failing spec and browser; avoid silently retrying tests until they pass.
- Use coverage to find code that has no tests, then add assertions for behavior and important edge cases. A high coverage percentage does not prove that the assertions are useful.
The available current Angular CI guidance does not establish a universal AngularJS command. Preserve and improve the command this repository already uses rather than assuming an Angular CLI workflow.
8. Troubleshooting common failures
| Symptom | Likely cause | What to do |
|---|---|---|
| The test command or browser cannot start | Runtime, browser, driver, or dependency versions no longer match the legacy configuration. | Read the lockfile and test configuration, use the runtime expected by the repository, and check its supported browser setup before upgrading packages. |
| AngularJS services or modules are undefined in a test | The test bootstrap did not load the app module or its dependencies, or the test bundle differs from the app bundle. | Compare the test bootstrap and module loading with a passing test in this repository. Confirm scripts and module names are loaded in the required order. |
| A browser test times out waiting for a result | The app failed to load, an expected response never arrived, the selector no longer matches, or the test waits on the wrong condition. | Check browser and server logs, verify the route and fixture data, then wait for the visible state the user should see. |
| A test passes alone but fails in the suite | Shared state, a spy, timer, mock, or browser session was not reset. | Restore state in teardown, make each test arrange its own data, and run the failing test alongside neighboring cases to isolate contamination. |
| The result changes between runs | A live network call, race, arbitrary sleep, or shared test data makes the outcome nondeterministic. | Control external responses and data; wait on explicit conditions; remove order dependencies. |
| Tests break after changing a selector or template | The tests rely on presentation details, or the change altered a user-facing control. | Use stable selectors associated with controls or accessible names. If the behavior changed, update the assertion to the intended user-visible result. |
| Protractor setup fails with a newer browser or runtime | The EOL toolchain may no longer track current browser changes or project dependencies. | Do not expand Protractor usage for a new suite. Keep maintenance scoped and plan migration to a runner verified against the app’s actual requirements. |
| Modern Angular test instructions do not work | AngularJS and modern Angular are distinct frameworks with different tooling and APIs. | Identify the installed framework and use documentation matching that framework and version. |
9. Keep the suite fast, reliable, and affordable to maintain
Unit tests are usually the cheapest tests to run and debug because they avoid browser startup and external services. Integration tests cost more but cover wiring that isolated functions cannot. Browser tests have the most setup and can be slower or more fragile, so reserve them for paths whose failure would materially affect users.
- Keep fixtures small and deterministic; avoid unnecessary shared test accounts or data.
- Run fast tests on each change and use browser tests for critical flows at the cadence your CI can support.
- Track flaky tests as defects: find the uncontrolled dependency and fix it rather than relying on retries.
- For browser-based visual checks, compare only stable pages and control dynamic content such as timestamps and rotating banners.
- Revisit the browser matrix when the app’s supported browsers or CI images change.
The main cost of an AngularJS test suite is often maintaining old dependencies and browser infrastructure, not the assertion count. Avoid adding a new legacy toolchain without checking its compatibility and long-term maintenance burden.
Or skip the browser setup
If you need screenshots of test or production pages for visual review, ScreenshotNeo returns a screenshot or PDF from one GET request. Its API documentation describes the request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners are accepted and removed before capture, along with known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, and failed loads are never billed; response headers report the page verdict and billing status. An MCP server gives AI agents screenshot, page information, and PDF capture tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
How do I test an AngularJS app?
Start with the project’s existing test command and build coverage across unit, integration, and a small number of browser-level user journeys. Verify versions and browser support before changing the toolchain.
How do I write unit tests in AngularJS?
Test a function or service through its inputs and outputs, keep external effects controlled, and use the test runner and AngularJS mocking APIs already compatible with the repository.
Can I use modern Angular’s testing guide for AngularJS?
No. AngularJS is a different framework generation. Use version-matched AngularJS references and the repository’s existing configuration.
Should a new AngularJS test suite use Protractor?
No. Protractor reached end of life in August 2023. Choose a maintained browser automation option only after verifying it supports the app’s browsers, CI environment, and legacy behavior.
Do I need browser tests for every feature?
No. Use browser tests for a few critical paths that benefit from exercising the whole running app. Test detailed edge cases at faster levels where practical.


