Search

#### Docs

- Guide
  - Overview
  - Setup
  - Learning Angular
  - Architecture
  - Displaying Data
  - User Input
  - Forms
  - Dependency Injection
  - Template Syntax
- Tutorial
  - Introduction
  - 0. The Starter App
  - 1. The Hero Editor
  - 2. Master/Detail
  - 3. Multiple Components
  - 4. Services
  - 5. Routing
  - 6. HTTP
- Advanced
  - Attribute Directives
  - Component Styles
  - Deployment
  - Hierarchical Injectors
  - HTTP Client
  - Lifecycle Hooks
  - Pipes
  - Router
  - Security
  - Structural Directives
  - Testing

#### Components

- Components Gallery

#### Resources

- Cheat Sheet
- Glossary
- FAQ
- Tools
- Community and Support

#### Reference

- Videos
- API Documentation

new_releases We're currently in the progress of migrating this site from Angular version 5.3.1 to 7.0.0. Package versions might be out-of-date or inconsistent! Visit the repo for more info.

# Component Testing: Page Objects (DRAFT) 6.0

If you notice any issues with this page, please report them.

<?code-excerpt path-base=”examples/ng/doc”?>

As components and their templates become more complex, you’ll want to separate concerns and isolate testing code from the detailed HTML encoding of page elements in templates.

You can achieve this separation by creating page object (PO) classes having APIs written in terms of application-specific concepts, such as “title”, “hero ID”, and “hero name”. A PO class encapsulates details about:

- HTML element access, for example, whether a hero name is contained in a heading element or a `<div>`
- Type conversions, for example, from `String` to `int`, as you’d need to do for a hero ID

## Pubspec configuration

The angular_test package recognizes page objects implemented using annotations from the pageloader package.

Add the package to the pubspec dependencies:

<?code-excerpt “toh-0/pubspec.yaml” diff-with=”toh-1/pubspec.yaml” from=”dev_dependencies” to=” test:”?>

    --- toh-0/pubspec.yaml
    +++ toh-1/pubspec.yaml
    @@ -11,6 +12,7 @@
     dev_dependencies:
       ngtest: ^5.0.0-dev.3
       build_runner: ^2.4.12
       build_test: ^2.1.5
       build_web_compilers: ^4.0.11
    +  ngpageloader: ^5.0.0
       test: ^1.21.0

## Imports

Include these imports at the top of your page object file:

<?code-excerpt “toh-1/test/app_po.dart (imports)” title?>

      import 'dart:async';

      import 'package:ngpageloader/pageloader.dart';

Update the imports at the top of your test file:

<?code-excerpt “toh-0/test/app_test.dart” diff-with=”toh-1/test/app_test.dart” to=”void main”?>

    --- toh-0/test/app_test.dart
    +++ toh-1/test/app_test.dart
    @@ -3,6 +3,9 @@
     import 'package:ngtest/ngtest.dart';
     import 'package:angular_tour_of_heroes/app_component.dart';
     import 'package:angular_tour_of_heroes/app_component.template.dart' as ng;
    +import 'package:ngpageloader/html.dart';
     import 'package:test/test.dart';

    +import 'app_po.dart';
    +
     void main() {

## Running example

As running examples, this page uses the Hero Editor and Heroes List apps from parts 1 and 2 of the tutorial, respectively.

You’ll first see POs and tests for the tutorial’s simple Hero Editor. Before proceeding, review the final code of the tutorial part 1 and take note of the app component template:

<?code-excerpt “toh-1/lib/app_component.dart (template)” title?>

      template: '''
        <h1>{{title}}</h1>
        <h2>{{hero.name}}</h2>
        <div><label>id: </label>{{hero.id}}</div>
        <div>
          <label>name: </label>
          <input [(ngModel)]="hero.name" placeholder="name">
        </div>
      ''',

You can use a single page object for an entire app when it is as simple as the Hero Editor. You might use such a page object to test the title like this:

<?code-excerpt “toh-1/test/app_test.dart (title)” title?>

      test('title', () {
        expect(appPO.title, 'Tour of Heroes');
      });

## PO class

Pageloader recognizes POs that satisfy the following conditions.

- Source file:
  - The file contains a `part` statement referring to the filename but with a `.g.dart` suffix.
  - The file doesn’t contain any Angular annotations (like `@Component` or `@GenerateInjector`) that would trigger the Angular builder. This is a temporary limitation; for details, see pageloader issue #134.
- PO class (declared in the source file):
  - `@PageObject()` annotates the class.
  - The class is `abstract`.
  - The class has these constructors:
    - A default constructor.
    - A factory constructor defined as shown below.

Here’s an example of a valid page object implementation:

<?code-excerpt “toh-1/test/app_po.dart (excerpt)” region=”boilerplate” title?>

      part 'app_po.g.dart';

      @PageObject()
      abstract class AppPO {

        AppPO();
        factory AppPO.create(PageLoaderElement context) = $AppPO.create;
        // ···
      }

During the build process, pageloader generates an implementation for your abstract PO class based on the fields you declare, and saves the implementation to the `*.g.dart` file. The generated code contains factory methods like `$AppPO.create`.

## PO field annotation basics

You can declaratively identify HTML elements that occur in a component’s template by adorning PO class getters with pageloader annotations like `@ByTagName('h1')`. For example, an initial version of `AppPO` might look like this:

<?code-excerpt “toh-1/test/app_po.dart (AppPO initial)” title replace=”/(@By|PageLoaderElement get).*/[!$&!]/g”?>

      @PageObject()
      abstract class AppPO {

        AppPO();
        factory AppPO.create(PageLoaderElement context) = $AppPO.create;

        [!@ByTagName('h1')!]
        [!PageLoaderElement get _title;!]
        // ···
        String get title => _title.visibleText;
        // ···
      }

Because of its @ByTagName() annotation, the `_h1` field will get bound to the app component template’s `<h1>` element.

Other basic tags, which you’ll soon see examples of, include:

- @ByClass()
- @ByCss()
- @ById()
- @First()
- @WithVisibleText()

The PO `title` field returns the heading element’s text.

## PO instantiation

Create PO instances using the PO factory constructor. PO fields are lazily initialized from a context passed as an argument to the constructor. Create a context from the fixture’s rootElement as shown below. Since most page objects are shared across tests, they are generally initialized during setup:

<?code-excerpt “toh-1/test/app_test.dart (appPO setup)” title replace=”/(final context |appPO |HtmlPageLoaderElement.).*/[!$&!]/g”?>

      final testBed = NgTestBed<AppComponent>(ng.AppComponentNgFactory);
      late NgTestFixture<AppComponent> fixture;
      late AppPO appPO;

      setUp(() async {
        fixture = await testBed.create();
        [!final context =!]
            [!HtmlPageLoaderElement.createFromElement(fixture.rootElement);!]
        [!appPO = AppPO.create(context);!]
      });

## Using POs in tests

When the Hero Editor app loads, it displays data for a hero named Windstorm having id 1. Here’s how you might test for this:

<?code-excerpt “toh-1/test/app_test.dart (hero)” title?>

      const windstormData = <String, dynamic>{'id': 1, 'name': 'Windstorm'};

      test('initial hero properties', () {
        expect(appPO.heroId, windstormData['id']);
        expect(appPO.heroName, windstormData['name']);
      });

After looking at the app component’s template, you might define the PO `heroId` and `heroName` fields like this:

<?code-excerpt “toh-1/test/app_po.dart (AppPO hero)” title?>

      abstract class AppPO {
        // ···
        @First(ByCss('div'))
        PageLoaderElement get _id; // e.g. 'id: 1'

        @ByTagName('h2')
        PageLoaderElement get _heroName;
        // ···
        int get heroId {
          final idAsString = _id.visibleText.split(':')[1];
          return int.tryParse(idAsString) ?? -1;
        }

        String get heroName => _heroName.visibleText;
        // ···
      }

The page object extracts the id from text that follows the “id:” label in the first `<div>`, and the hero name from the `<h2>` text.

#### PO field bindings are lazily initialized and final

PO fields are bound when the field is first accessed, based on the state of the fixture’s root element. Once bound, they do not change.

## PO List fields

The app from part 2 of the tutorial displays a list of heros, generated using an `ngFor` applied to an `<li>` element:

<?code-excerpt “toh-2/lib/app_component.html” remove=”/h1|div|input|label|selected.name/” title?>

      <h2>Heroes</h2>
      <ul class="heroes">
        <li *ngFor="let hero of heroes"
            [class.selected]="hero == selected"
            (click)="onSelect(hero)">
          <span class="badge">{{hero.id}}</span> {{hero.name}}
        </li>
      </ul>
        <h2>{{selected!.name}}</h2>

To define a PO field that collects all generated `<li>` elements, use the annotations introduced earlier, but declare the field to be of type `List<PageLoaderElement>`:

<?code-excerpt “toh-2/test/app_po.dart (_heroes)” title?>

      @ByTagName('li')
      List<PageLoaderElement> get _heroes;

When bound, the `_heroes` list will contain an element for each `<li>` in the view. If the displayed heroes list is empty, then `_heroes` will be an empty list — `List<PageLoaderElement>` PO fields are never `null`.

You might render hero data (as a map) from the text of the `<li>` elements like this:

<?code-excerpt “toh-2/test/app_po.dart (heroes)” title?>

      Iterable<Map> get heroes =>
          _heroes.map((el) => _heroDataFromLi(el.visibleText));

      // ···
      Map<String, dynamic> _heroDataFromLi(String liText) {
        final matches = RegExp((r'^(\d+) (.*)$')).firstMatch(liText);
        return _heroData(matches?[1], matches?[2]);
      }

## PO optional fields

Only once a hero is selected from the Heroes List, are the selected hero’s details displayed using this template fragment:

<?code-excerpt “toh-2/lib/app_component.html” remove=”/h1|[Hh]ero|li|ul\x3E/” title?>

      <div *ngIf="selected != null">
        <h2>{{selected!.name}}</h2>
        <div><label>id: </label>{{selected!.id}}</div>
        <div>
          <label>name: </label>
          <input [(ngModel)]="selected!.name" placeholder="name">
        </div>
      </div>

Declare an optional field like any other field:

<?code-excerpt “toh-2/test/app_po.dart (hero detail ID)” title?>

      @First(ByCss('div div'))
      PageLoaderElement get _heroDetailId;

To determine whether an optionally displayed page element is present, test its `exists` property:

<?code-excerpt “toh-2/test/app_po.dart (heroFromDetails)” title replace=”/\w+.exists/[!$&!]/g”?>

      Map<String, dynamic>? get heroFromDetails {
        if (![!_heroDetailId.exists!]) return null;
        // ···
      }