Aller au contenu

Testing the App

Copy of sumaris-app/.agents/rules/testing.md (rule written for AI coding agents, when testing the application or reproducing bugs). Do not edit here: run npm run site:sync-rules to update it.

Reproduce a bug, or test a fix or an enhancement

Use the tool Playwright, to test the App on http://localhost:4200 (see navigation to find a page or a data).

Related skills (in .agents/skills/):

  • playwright-cli: drive the browser from the terminal — use the local version npx playwright cli <command> (no global install needed);
  • chrome-devtools: debug with Chrome DevTools (console, network, performance) — requires the chrome-devtools MCP server (see .agents/mcp/mcp.example.json);
  • test-driven-development: write a failing test first (Jasmine unit test, or e2e spec), then the fix.

End-to-end testing

The testing configuration is defined in the playwright.config.ts file, which includes settings for running tests in a web server environment.

The configuration dynamically determines whether to use a web server based on the base URL provided in the environment variables or defaults to http://localhost:4200.

Running E2E tests

  • npx playwright test — Run all E2E tests with the default base URL (http://localhost:4200)
  • npx playwright test e2e/auth.spec.ts — Run a specific test file
  • npx playwright test e2e/projects/pifil — Run the tests of a program
  • npx playwright test --headed — Run tests in headed mode (visible browser)
  • npx playwright test --ui — Run tests with the Playwright UI

Remember to add --reporter=line to avoid html report to be opened automatically.

Testing against a different base URL

  • PLAYWRIGHT_BASE_URL=https://my-server.example.com npx playwright test — Target a remote or different server
  • PLAYWRIGHT_WEB_SERVER_COMMAND="npm run start" npx playwright test — Override the web server start command for local testing

When the base URL points to a non-local server (not localhost or 127.0.0.1), the Playwright web server is automatically disabled — the tests run directly against the provided URL.

Test framework (@sumaris-net/ngx-components/testing)

E2E tests use the testing framework of ngx-sumaris-components (package @sumaris-net/ngx-components/testing, sources in ngx-sumaris-components/testing/):

  • UiHelper, IonicHelper, MaterialHelper (and IonicSelectors, MaterialSelectors): generic helpers — extended in e2e/utils/ (AppUiHelper, AppIonicHelper, AppMaterialHelper) only for missing features;
  • AuthHelper.loginByEnv(): auto login at the app startup, by giving credentials to the app through the window object;
  • withEnvs(): reads the login environments (peers and users, with tags) from a JSON file.

Authentication (user profiles)

  • The app must be served with the test configuration (ng serve --configuration test, the default of playwright.config.ts): its environment declares startupOrder: ['url', 'window'], so the app reads the credentials given by the tests (the startup is registered in app.module.ts by provideAppInitializer(createAppStartupInitializer()));
  • Each test is logged in automatically, at the app startup, with the profile option of e2e/fixtures.ts (default: admin): no need to log in in a test;
  • To run as another profile: test.use({ profile: 'observer' }) (admin, supervisor or observer);
  • To stay logged out (e.g. to test the login from the UI, with auth.loginAs(profile)): test.use({ profile: null });
  • Users (and the peer URL) are declared in e2e/.environments.json (tags local for the peer, and admin/supervisor/observer for the users), from the pod test database (sumaris-pod/sumaris-test-shared/src/main/resources/data-hsqldb-*.xml, tables PERSON, PERSON2USER_PROFILE);
  • To test another server: add a peer (with its own tag) in .local/environments.json (git ignored, read first) or in the file given by LOGIN_ENVS_PATH, then run with E2E_PEER=<tag>.

Structure

e2e/
├── fixtures.ts                  # `test` and `expect` to import in every spec (auto login + helpers below)
├── global-setup.ts              # Check the login environments, before all tests
├── .environments.json           # Login environments: peers and users (by profile tag)
├── utils/                       # Helpers (page objects) — always use them, instead of raw selectors
│   ├── navigation.utils.ts      # NavigationHelper: open root tables (from the menu), filtered by program
│   ├── table.utils.ts           # TableHelper: rows, filter panel, "add" button, readonly checks
│   ├── editor.utils.ts          # EditorHelper: tabs, save/close, quality card, readonly checks
│   ├── menu.utils.ts            # MenuHelper + MenuTitles: side menu entries
│   ├── auth.utils.ts            # AppAuthHelper (extends AuthHelper): login from the UI, login state, logout
│   ├── env.utils.ts             # User profiles, and their login environment (`withEnvs()`)
│   ├── ui.utils.ts              # AppUiHelper (extends UiHelper): buttons, form fields
│   ├── material.utils.ts        # AppMaterialHelper (extends MaterialHelper): scoped form fields, autocomplete
│   ├── ionic.utils.ts           # AppIonicHelper (extends IonicHelper): alerts; AppIonicSelectors
│   └── app.utils.ts             # AppHelper: settings storage
├── projects/<program>/          # One folder per program (lowercase program label, e.g. `sih-obsmer`)
│   ├── trips.spec.ts            # Menu "Marées"
│   ├── observations.spec.ts     # Menu "Sorties"
│   └── activity-calendars.spec.ts
└── *.spec.ts                    # Tests not related to a program (auth, settings, vessels, ...)

Writing an e2e test

  • Import test and expect from e2e/fixtures.ts (not from @playwright/test), then use the fixtures: nav, table, editor, menu, auth, ui;
  • Navigate like a user (see navigation): open the root table from the menu (nav.openTrips({ program })), click a row, then open the tabs;
  • Each program (column "Programme" of the root tables) must have a spec for each menu entry where it has data ("Marées", "Sorties"...);
  • Check the readonly state: editor.expectEditable() / editor.expectReadOnly() (inputs of the selected tab), table.expectEditable() / table.expectReadOnly() ("add" button); a validated data must be readonly, including its children;
  • Keep specs readable: one comment per step, business helpers instead of raw selectors. If a selector is needed in several specs, add a method to a helper (tables, toolbars, filter panels, editors share the same HTML/CSS in the whole app);
  • Match labels in both languages (e.g. /Opérations|Operations/i): the language depends on the user account settings;
  • Never use page.waitForTimeout(): wait for a state (expect(...).toBeVisible(), table.waitForLoaded(), editor.waitForLoaded());
  • Document a known bug with test.fail(true, '<reason>') (the test then passes while the bug exists), and a missing test data with test.skip(condition, '<reason>').
  • Fields without formcontrolname (bound with [formControl], PMFM fields...) are found by their label: ui.fillField(), ui.selectByLabel() (mat-select or autocomplete), ui.selectAutocompleteOptionByLabel(), ui.setDateTime(), ui.setBoolean(); use ui.section('<row label>') as scope, to disambiguate fields with the same label (e.g. the start and end dates of an operation);
  • To investigate a problem, enable the debug mode of a page with editor.enableDebug() (options menu ⋮ > "Débogage..." > "Activer le débogage ?"): it displays debug panels (app-debug) and enables debug logs; listen to page.on('console') and page.on('pageerror') to read them. In the test configuration, components can also be inspected with window.ng.getComponent(element);
  • Filter a table with table.filterByProgram(), table.filterByVessel(), table.filterBySearchText(), or table.applyFilter(panel => ...) for other fields;
  • Tests that create data must delete it, even if the test fails: remember the id after the first save (editor.idFromUrl()), then delete it in test.afterEach() (editor.delete()). Read tests must not depend on the row order (e.g. use table.row('01/12/2021') instead of table.clickRow(0)), because other tests may create data in parallel;
// GPL v3 License
import { expect, test } from '../../fixtures';

const PROGRAM = 'PIFIL';

test.describe(`${PROGRAM} > Trips`, () => {
  test.beforeEach(async ({ nav }) => {
    // Open the trips table ("Marées"), filtered by program
    await nav.openTrips({ program: PROGRAM });
  });

  test('should open a trip, then an operation', async ({ table, editor }) => {
    // Open the trip
    await table.clickRow(0);
    await editor.waitForLoaded(/\/trips\/\d+$/);
    await editor.expectEditable();

    // Operations tab, then open the first operation
    const operations = await editor.openTab(/Opérations|Operations/i);
    await operations.clickRow(0);
    await editor.waitForLoaded(/\/trips\/\d+\/operation\/\d+$/);
  });
});

Testing pages (in-app component demos)

Each reusable component can have a testing page — an in-app demo accessible at /testing/shared/<name>.

Structure

src/app/shared/<module>/testing/           # or src/app/shared/components/<name>/testing/
├── <name>.testing.ts                      # Component (declared in NgModule, not standalone)
├── <name>.testing.html                    # Template with component variations
├── <name>.testing.scss                    # Styles
└── <name>.testing.module.ts               # NgModule + TESTING_PAGES const + routes

*.testing.module.ts pattern

import { TestingPage } from '@sumaris-net/ngx-components';
import { TranslatePipe } from '@ngx-translate/core';

export const XXX_TESTING_PAGES: TestingPage[] = [
  { label: 'My Component', page: '/testing/shared/xxx' }
];

const routes: Routes = [
  { path: '', pathMatch: 'full', component: XxxTestingPage },
];

@NgModule({
  imports: [
    CommonModule,
    AppSharedModule,
    CoreModule,
    TranslatePipe,
    RouterModule.forChild(routes),
    XxxComponent, // standalone components used by the page go in `imports`
  ],
  declarations: [XxxTestingPage],
  exports: [XxxTestingPage],
})
export class AppXxxTestingModule {}

Wiring into parent testing module

In src/app/shared/shared.testing.module.ts:

  1. Import XXX_TESTING_PAGES
  2. Spread into APP_SHARED_TESTING_PAGES
  3. Add lazy route: { path: 'xxx', loadChildren: () => import('./xxx/testing/xxx.testing.module').then(m => m.AppXxxTestingModule) }

Route hierarchy

  • /testing → redirects to /testing/shared
  • /testing/shared → AppSharedTestingModule (lists all shared testing pages)
  • /testing/shared/xxx → lazy loads AppXxxTestingModule

Page layout pattern

Testing pages typically render a grid of variation cards, each isolating one input or combination of inputs:

<ion-toolbar color="primary">
  <ion-title>Component demo</ion-title>
  <ion-buttons slot="end">
    <ion-button (click)="toggleDebug()">Debug</ion-button>
  </ion-buttons>
</ion-toolbar>

<ion-content class="ion-padding">
  <h3>Variation group</h3>
  <div class="cards-grid">
    @for (card of cards; track card.title) {
      <div class="card">
        <div class="card-title">{{ card.title }}</div>
        <div class="card-desc">{{ card.description }}</div>
        <div class="card-render"><app-xxx [input]="card.input"></app-xxx></div>
      </div>
    }
  </div>
</ion-content>

Define one variation array per input (rotation, format, size, ...) in the page component, and add an ichthyometerCards style array for realistic combined-input examples.