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: runnpm run site:sync-rulesto 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 versionnpx playwright cli <command>(no global install needed);chrome-devtools: debug with Chrome DevTools (console, network, performance) — requires thechrome-devtoolsMCP 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 filenpx playwright test e2e/projects/pifil— Run the tests of a programnpx 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 serverPLAYWRIGHT_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(andIonicSelectors,MaterialSelectors): generic helpers — extended ine2e/utils/(AppUiHelper,AppIonicHelper,AppMaterialHelper) only for missing features;AuthHelper.loginByEnv(): auto login at the app startup, by giving credentials to the app through thewindowobject;withEnvs(): reads the login environments (peers and users, with tags) from a JSON file.
Authentication (user profiles)¶
- The app must be served with the
testconfiguration (ng serve --configuration test, the default ofplaywright.config.ts): its environment declaresstartupOrder: ['url', 'window'], so the app reads the credentials given by the tests (the startup is registered inapp.module.tsbyprovideAppInitializer(createAppStartupInitializer())); - Each test is logged in automatically, at the app startup, with the
profileoption ofe2e/fixtures.ts(default:admin): no need to log in in a test; - To run as another profile:
test.use({ profile: 'observer' })(admin,supervisororobserver); - 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(tagslocalfor the peer, andadmin/supervisor/observerfor the users), from the pod test database (sumaris-pod/sumaris-test-shared/src/main/resources/data-hsqldb-*.xml, tablesPERSON,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 byLOGIN_ENVS_PATH, then run withE2E_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
testandexpectfrome2e/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 withtest.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(); useui.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 topage.on('console')andpage.on('pageerror')to read them. In thetestconfiguration, components can also be inspected withwindow.ng.getComponent(element); - Filter a table with
table.filterByProgram(),table.filterByVessel(),table.filterBySearchText(), ortable.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 intest.afterEach()(editor.delete()). Read tests must not depend on the row order (e.g. usetable.row('01/12/2021')instead oftable.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:
- Import
XXX_TESTING_PAGES - Spread into
APP_SHARED_TESTING_PAGES - 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 loadsAppXxxTestingModule
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.