@sumaris-net/ngx-components 21.4.4 → 21.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/doc/changelog.md CHANGED
@@ -2510,3 +2510,19 @@ The library now uses the Ionic **standalone** build (`@ionic/angular/standalone`
2510
2510
  * Result count under the search bar (`COMMON.RESULT_COUNT` / `COMMON.NO_RESULT`), hidden when the filter is empty
2511
2511
  * Keyboard: `/` or `Ctrl+K` to open the search bar, `↑`/`↓` + `Enter` to open a page, `Esc` to clear (then close)
2512
2512
  * Search text kept in the URL (`?q=`), restored when going back from a test page
2513
+
2514
+ ## 21.4.5
2515
+ - fix(register): stop logging the registration payload in the console, it exposed the password in clear (closes #17)
2516
+ - fix(autocomplete-field) Fix focus lost when clicking on an option, inside a modal - closes sumaris-app#2218
2517
+ - fix(table): `app-actions-column` and `app-nav-actions-column`: fix keyboard navigation when holding down Tab or Shift+Tab (focus leaving the table, rows skipped or edited in the wrong order)
2518
+ * During a navigation, the Tab key is locked until the focus reaches a cell (new input `navigationTimeout`, default: 300 ms)
2519
+ * The focus never stays on the invisible navigation buttons: if needed, it is moved to the target row, or back to the edited row
2520
+ * **Breaking**: remove input `throttleTime` of `app-nav-actions-column`
2521
+ - fix(testing): table test page: fix the validators of `date` and `latitude` (declared as async validators, the row was always pending), and generate values for these fields
2522
+ - fix(table): align `AppTable` on `AppAsyncTable`
2523
+ * Fix `cancelExistingRow()` (inverted `interactive` logic: never asked for confirmation, and looped when `onBeforeCancelRows` was observed)
2524
+ * Add `confirmAndUp()`, options `keepSelection`, `confirmRow` and `throwIfNotConfirmed` in `save()`, `logPrefix`, `reservedStartColumns` and `reservedEndColumns`
2525
+ * `save({ keepEditing: true })` now also restores the first row (id = 0); `confirmAndForward()` and `confirmAndAdd()` no longer fail without any row
2526
+
2527
+ ## 21.4.6
2528
+ - fix(table): `AppTable`: fix a reload while a new row is in edition: the last row was deleted, in addition to the new row (and then lost on save)
@@ -93,6 +93,8 @@ const MaterialSelectors = {
93
93
  tabGroup: 'mat-tab-group',
94
94
  /** Material tab label */
95
95
  tabLabel: '.mat-mdc-tab',
96
+ /** Material tab link (tab nav bar) */
97
+ tabLink: '.mat-mdc-tab-link',
96
98
  /** Material paginator */
97
99
  paginator: 'mat-paginator',
98
100
  /** Material paginator range label (e.g. '1 - 20 of 42') */
@@ -105,6 +107,12 @@ const MaterialSelectors = {
105
107
  autocompletePanel: '.mat-mdc-autocomplete-panel',
106
108
  /** Material autocomplete option */
107
109
  autocompleteOption: 'mat-option',
110
+ /** Material autocomplete trigger (input) */
111
+ autocompleteTrigger: 'input.mat-mdc-autocomplete-trigger',
112
+ /** Material select panel */
113
+ selectPanel: '.mat-mdc-select-panel',
114
+ /** Material select trigger */
115
+ selectTrigger: '.mat-mdc-select-trigger',
108
116
  };
109
117
  /**
110
118
  * Helper functions for interacting with Angular Material components.
@@ -442,6 +450,139 @@ class AuthHelper {
442
450
  }
443
451
  }
444
452
 
453
+ /** Library defaults */
454
+ const DEFAULT_OPTIONS = {
455
+ padding: { left: '16px', right: '16px' },
456
+ noPaddingSelector: '.ion-no-padding, [no-padding], .mat-autocomplete-searchbar',
457
+ };
458
+ const NO_PADDING = { left: '0px', right: '0px' };
459
+ const TAB = `${MaterialSelectors.tabLink}:not(.mat-mdc-tab-disabled)`;
460
+ const ACTIVE_TAB = '.mdc-tab--active';
461
+ const FIELD_TRIGGER = [
462
+ `${MaterialSelectors.autocompleteTrigger}:not([disabled]):not([readonly])`,
463
+ `mat-select:not(.mat-mdc-select-disabled) ${MaterialSelectors.selectTrigger}`,
464
+ ].join(', ');
465
+ const OPEN_PANEL = `${MaterialSelectors.autocompletePanel}, ${MaterialSelectors.selectPanel}`;
466
+ const LOADED_OPTION = `${MaterialSelectors.autocompleteOption}:not(:has(${IonicSelectors.skeletonText}))`;
467
+ const PANEL_TIMEOUT = 3_000;
468
+ /**
469
+ * Check the padding of `mat-option`, on any page.
470
+ *
471
+ * Nothing here depends on a given field: tabs, fields and options are discovered from the DOM,
472
+ * using the CSS classes set by Angular Material. Test cases can be added to (or removed from)
473
+ * a page without changing the tests.
474
+ */
475
+ class OptionPaddingHelper {
476
+ page;
477
+ options;
478
+ constructor(page, options) {
479
+ this.page = page;
480
+ this.options = { ...DEFAULT_OPTIONS, ...options };
481
+ }
482
+ /** Wait for the page to display at least one field */
483
+ async waitForFields(timeout = 30_000) {
484
+ await this.page.locator(FIELD_TRIGGER).first().waitFor({ state: 'attached', timeout });
485
+ }
486
+ /** Open the panel of every field of every tab of the current page, and check all of its options */
487
+ async checkAllFields() {
488
+ const report = { fieldCount: 0, optionCount: 0, violations: [] };
489
+ const tabs = await this.page.locator(TAB).all();
490
+ // Pages without tabs have a single "anonymous" view
491
+ for (const tab of tabs.length ? tabs : [null]) {
492
+ if (tab) {
493
+ await tab.click();
494
+ await tab.and(this.page.locator(ACTIVE_TAB)).waitFor();
495
+ }
496
+ const tabName = tab ? (await tab.innerText()).trim() : '';
497
+ await this.checkFieldsOfCurrentView(tabName, report);
498
+ }
499
+ return report;
500
+ }
501
+ /**
502
+ * Check the option padding without any field, using fake options added to the page. Their parent is a panel
503
+ * inheriting from `ion-no-padding`, like an inline panel inside a form grid. This catches a global rule leaking
504
+ * from any component loaded by the page (e.g. `::ng-deep .mat-mdc-option`), or padding vars inherited from a parent.
505
+ */
506
+ async checkProbes() {
507
+ const violations = [];
508
+ for (const noPadding of [true, false]) {
509
+ const { left, right } = await this.page.evaluate((isNoPadding) => {
510
+ const parent = document.createElement('div');
511
+ parent.className = `ion-no-padding ${isNoPadding ? 'mat-mdc-autocomplete-panel' : 'mat-mdc-select-panel'}`;
512
+ const option = parent.appendChild(document.createElement('mat-option'));
513
+ option.className = isNoPadding ? 'mat-mdc-option ion-no-padding' : 'mat-mdc-option';
514
+ document.body.appendChild(parent);
515
+ const { paddingLeft, paddingRight } = getComputedStyle(option);
516
+ parent.remove();
517
+ return { left: paddingLeft, right: paddingRight };
518
+ }, noPadding);
519
+ const violation = this.checkPadding('Probe option', left, right, noPadding);
520
+ if (violation)
521
+ violations.push(violation);
522
+ }
523
+ return violations;
524
+ }
525
+ async checkFieldsOfCurrentView(tabName, report) {
526
+ const triggers = await this.page.locator(FIELD_TRIGGER).filter({ visible: true }).all();
527
+ for (const trigger of triggers) {
528
+ report.fieldCount++;
529
+ const field = `[${tabName}] "${await this.getFieldName(trigger)}"`;
530
+ const panel = await this.openPanel(trigger);
531
+ if (!panel)
532
+ continue; // The field has no panel to show (e.g. no item)
533
+ for (const { text, noPadding, left, right } of await this.readOptions(panel)) {
534
+ report.optionCount++;
535
+ const violation = this.checkPadding(`${field} > option "${text}"`, left, right, noPadding);
536
+ if (violation)
537
+ report.violations.push(violation);
538
+ }
539
+ await this.closePanel(panel);
540
+ }
541
+ }
542
+ checkPadding(where, left, right, noPadding) {
543
+ const expected = noPadding ? NO_PADDING : this.options.padding;
544
+ return left === expected.left && right === expected.right
545
+ ? null
546
+ : `${where}: padding left/right is ${left}/${right}, expected ${expected.left}/${expected.right} (${noPadding ? 'no-padding option' : 'option with padding'})`;
547
+ }
548
+ getFieldName(trigger) {
549
+ return trigger.evaluate((el) => {
550
+ const label = el.closest('mat-form-field')?.querySelector('mat-label')?.textContent;
551
+ const name = label || el.getAttribute('placeholder') || el.closest('mat-select')?.getAttribute('aria-label');
552
+ return name?.trim() || el.tagName.toLowerCase();
553
+ });
554
+ }
555
+ /** Open the field's panel. Dispatch the event, because some triggers ignore the pointer until focused (e.g. lat/long sign) */
556
+ async openPanel(trigger) {
557
+ await trigger.scrollIntoViewIfNeeded();
558
+ await trigger.focus();
559
+ await trigger.dispatchEvent('click');
560
+ const panel = this.page.locator(OPEN_PANEL).filter({ visible: true }).first();
561
+ try {
562
+ await panel.locator(LOADED_OPTION).first().waitFor({ state: 'attached', timeout: PANEL_TIMEOUT });
563
+ return panel;
564
+ }
565
+ catch {
566
+ return null;
567
+ }
568
+ }
569
+ async closePanel(panel) {
570
+ await this.page.keyboard.press('Escape');
571
+ await panel.waitFor({ state: 'hidden', timeout: PANEL_TIMEOUT });
572
+ }
573
+ readOptions(panel) {
574
+ return panel.locator(LOADED_OPTION).evaluateAll((options, noPaddingSelector) => options.map((option) => {
575
+ const { paddingLeft, paddingRight } = getComputedStyle(option);
576
+ return {
577
+ text: (option.textContent ?? '').trim().slice(0, 40),
578
+ noPadding: option.matches(noPaddingSelector),
579
+ left: paddingLeft,
580
+ right: paddingRight,
581
+ };
582
+ }), this.options.noPaddingSelector);
583
+ }
584
+ }
585
+
445
586
  // GPL v3 License
446
587
  /**
447
588
  * Search environments based on provided criteria.
@@ -566,5 +707,5 @@ function testWithEnv(env) {
566
707
  * Generated bundle index. Do not edit.
567
708
  */
568
709
 
569
- export { AuthHelper, IonicHelper, IonicSelectors, MaterialHelper, MaterialSelectors, STARTUP_DATA_STORAGE_KEY, UiHelper, testWithEnv, withEnvs };
710
+ export { AuthHelper, IonicHelper, IonicSelectors, MaterialHelper, MaterialSelectors, OptionPaddingHelper, STARTUP_DATA_STORAGE_KEY, UiHelper, testWithEnv, withEnvs };
570
711
  //# sourceMappingURL=sumaris-net-ngx-components-testing.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sumaris-net-ngx-components-testing.mjs","sources":["../../testing/utils/startup.constants.ts","../../testing/utils/ionic.utils.ts","../../testing/utils/material.utils.ts","../../testing/utils/ui.utils.ts","../../testing/utils/auth.utils.ts","../../testing/test/login-env.ts","../../testing/testing_api.ts","../../testing/sumaris-net-ngx-components-testing.ts"],"sourcesContent":["// Standalone startup constants for the testing package.\n// Must be kept in sync with src/app/core/services/startup.constants.ts\nexport const STARTUP_DATA_STORAGE_KEY = '__appStartupData';\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\n\n/**\n * Utility selectors and helpers for Ionic components in e2e tests.\n */\nexport const IonicSelectors = {\n /** Ionic button */\n button: 'ion-button',\n /** Ionic modal */\n modal: 'ion-modal',\n /** Ionic content area */\n content: 'ion-content',\n /** Ionic item */\n item: 'ion-item',\n /** Ionic label */\n label: 'ion-label',\n /** Ionic spinner */\n spinner: 'ion-spinner',\n /** Ionic skeleton text (loading placeholder) */\n skeletonText: 'ion-skeleton-text',\n /** Ionic back button */\n backButton: 'ion-back-button',\n /** Ionic toolbar */\n toolbar: 'ion-toolbar',\n /** Ionic footer */\n footer: 'ion-footer',\n /** Ionic header */\n header: 'ion-header',\n};\n\n/**\n * Helper functions for interacting with Ionic components.\n */\nexport class IonicHelper {\n constructor(private page: Page) {}\n\n /** Get an Ionic button by text content */\n buttonByText(text: string | RegExp): Locator {\n return this.page.locator(IonicSelectors.button, { hasText: text });\n }\n\n /** Get an Ionic button by CSS selector */\n button(selector?: string, options?: { hasText?: string | RegExp }): Locator {\n const fullSelector = selector ? `${IonicSelectors.button}${selector}` : IonicSelectors.button;\n return this.page.locator(fullSelector, options);\n }\n\n /**\n * Get an Ionic modal by its inner component selector (e.g. 'app-auth-form').\n * Automatically wraps the selector in `ion-modal:has(...)` so callers\n * only need to provide the component tag name.\n */\n modal(selector: string): Locator {\n const fullSelector = selector.startsWith('ion-modal') ? selector : `ion-modal:has(${selector})`;\n return this.page.locator(fullSelector);\n }\n\n /** Wait for the Ionic app to be ready */\n async waitForAppReady(timeout = 15_000): Promise<void> {\n // Wait for ion-app to be attached (Angular bootstrap complete)\n await this.page.waitForSelector('ion-app', { state: 'attached', timeout });\n }\n\n /** Wait for loading spinners to disappear */\n async waitForLoadingComplete(timeout = 15_000): Promise<void> {\n await this.page\n .locator(IonicSelectors.spinner)\n .waitFor({ state: 'hidden', timeout })\n .catch(() => {\n // Spinner may not appear at all — that's fine\n });\n }\n}\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\n\n/**\n * Utility selectors and helpers for Angular Material components in e2e tests.\n */\nexport const MaterialSelectors = {\n /** Material table row */\n row: 'mat-row, tr[mat-row]',\n /** Material header row */\n headerRow: 'mat-header-row, tr[mat-header-row]',\n /** Material cell */\n cell: 'mat-cell, td[mat-cell]',\n /** Material form field */\n formField: 'mat-form-field',\n /** Material input */\n input: 'input[matInput]',\n /** Material tab group */\n tabGroup: 'mat-tab-group',\n /** Material tab label */\n tabLabel: '.mat-mdc-tab',\n /** Material paginator */\n paginator: 'mat-paginator',\n /** Material paginator range label (e.g. '1 - 20 of 42') */\n paginatorRangeLabel: '.mat-mdc-paginator-range-label',\n /** Material progress bar */\n progressBar: 'mat-progress-bar',\n /** Material icon */\n icon: 'mat-icon',\n /** Material autocomplete panel */\n autocompletePanel: '.mat-mdc-autocomplete-panel',\n /** Material autocomplete option */\n autocompleteOption: 'mat-option',\n};\n\n/**\n * Helper functions for interacting with Angular Material components.\n */\nexport class MaterialHelper {\n constructor(private page: Page) {}\n\n /** Append a suffix (e.g. a class) to each part of a selector list (e.g. 'mat-cell, td[mat-cell]') */\n protected static withSuffix(selectors: string, suffix: string): string {\n return selectors\n .split(',')\n .map((selector) => selector.trim() + suffix)\n .join(', ');\n }\n\n /** Get all visible table rows, optionally scoped to a parent locator */\n tableRows(scope?: Locator): Locator {\n const root = scope ?? this.page;\n return root.locator(MaterialSelectors.row);\n }\n\n /** Get a single table row by index (0-based), optionally scoped to a parent locator */\n tableRow(index: number, scope?: Locator): Locator {\n return this.tableRows(scope).nth(index);\n }\n\n /** Click on a table row by index (0-based), optionally scoped to a parent locator */\n async clickRow(index: number, scope?: Locator): Promise<void> {\n await this.tableRow(index, scope).click();\n }\n\n /** Get a tab by its label text */\n tabByLabel(text: string | RegExp): Locator {\n return this.page.locator(MaterialSelectors.tabLabel, { hasText: text });\n }\n\n /** Click a tab by its label text */\n async clickTab(text: string | RegExp): Promise<void> {\n await this.tabByLabel(text).click();\n }\n\n /** Fill a mat-form-field input identified by its formControlName.\n * Supports both native inputs with formcontrolname and custom components (e.g. mat-autocomplete-field) wrapping an input. */\n async fillFormControl(formControlName: string, value: string): Promise<void> {\n // Try native input first, then fall back to custom component wrapper\n const nativeInput = this.page.locator(`${MaterialSelectors.formField} input[formcontrolname=\"${formControlName}\"]`);\n const wrappedInput = this.page.locator(`[formcontrolname=\"${formControlName}\"] input`);\n const input = (await nativeInput.count()) > 0 ? nativeInput : wrappedInput;\n // Click the parent mat-form-field to ensure focus reaches the input\n const formField = input.locator('xpath=ancestor::mat-form-field');\n if ((await formField.count()) > 0) {\n await formField.first().click();\n } else {\n await input.click({ force: true });\n }\n await input.pressSequentially(value, { delay: 50 });\n }\n\n /**\n * Select an option from a mat-select dropdown by formControlName.\n * Opens the dropdown, waits for options to render, clicks the matching option, then waits for the dropdown to close.\n */\n async selectMatSelectOption(formControlName: string, optionText: string | RegExp): Promise<void> {\n const select = this.page.locator(`mat-select[formcontrolname=\"${formControlName}\"]`);\n await select.click();\n // Use role-based selector: mat-select renders options with role=\"option\" in a listbox\n const option = this.page.getByRole('option', { name: optionText });\n await option.waitFor({ state: 'visible', timeout: 10_000 });\n await option.click();\n // Wait for the option to disappear (dropdown closed)\n await option.waitFor({ state: 'hidden', timeout: 5_000 });\n }\n\n /** Select an autocomplete option by text after typing in a form control */\n async selectAutocompleteOption(formControlName: string, searchText: string, optionText: string | RegExp): Promise<void> {\n await this.fillFormControl(formControlName, searchText);\n const panel = this.page.locator(MaterialSelectors.autocompletePanel);\n await panel.waitFor({ state: 'visible', timeout: 10_000 });\n await panel.locator(MaterialSelectors.autocompleteOption, { hasText: optionText }).click();\n }\n\n /** Get the cells of a table column (e.g. 'lastName'), optionally scoped to a parent locator */\n tableCells(column: string, scope?: Locator): Locator {\n return this.tableRows(scope).locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`));\n }\n\n /** Get the cell of a row, in the given column (e.g. 'lastName') */\n rowCell(row: Locator, column: string): Locator {\n return row.locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`));\n }\n\n /** Get the table rows having a cell (in the given column) that contains the text, optionally scoped to a parent locator */\n tableRowsByCell(column: string, text: string | RegExp, scope?: Locator): Locator {\n return this.tableRows(scope).filter({\n has: this.page.locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`), { hasText: text }),\n });\n }\n\n /** Get the header cell of a table column (e.g. to sort it), optionally scoped to a parent locator */\n tableHeaderCell(column: string, scope?: Locator): Locator {\n const root = scope ?? this.page;\n return root.locator(MaterialHelper.withSuffix(MaterialSelectors.headerRow, ` .mat-column-${column}`));\n }\n\n /** Read the total count, from the paginator range label (e.g. '1 - 20 of 42' -> 42), optionally scoped to a parent locator */\n async paginatorTotal(scope?: Locator): Promise<number> {\n const root = scope ?? this.page;\n const label = root.locator(`${MaterialSelectors.paginator} ${MaterialSelectors.paginatorRangeLabel}`).first();\n await label.waitFor({ state: 'visible', timeout: 10_000 });\n const text = ((await label.textContent()) ?? '').trim();\n const total = Number(text.split(/\\s+/).pop());\n return isNaN(total) ? 0 : total;\n }\n\n /** Wait for the progress bar(s) to stop (not in 'query' mode), optionally scoped to a parent locator */\n async waitForProgressBarIdle(timeout = 15_000, scope?: Locator): Promise<void> {\n const root = scope ?? this.page;\n await root.locator(`${MaterialSelectors.progressBar}[mode=\"query\"]`).first().waitFor({ state: 'detached', timeout });\n }\n\n /** Wait for table rows to be loaded (at least one row visible), optionally scoped to a parent locator */\n async waitForTableRows(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.tableRows(scope).first().waitFor({ state: 'visible', timeout });\n }\n}\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\nimport { IonicHelper, IonicSelectors } from './ionic.utils';\nimport { MaterialHelper, MaterialSelectors } from './material.utils';\n\n/**\n * Common UI helper functions to improve readability of e2e tests.\n * This helper centralizes access to Ionic and Material specific helpers.\n */\nexport class UiHelper {\n protected ionic: IonicHelper;\n protected mat: MaterialHelper;\n\n constructor(protected page: Page) {\n this.ionic = new IonicHelper(page);\n this.mat = new MaterialHelper(page);\n }\n\n /**\n * Click an element and wait for it to be ready.\n * @param locator The locator to click.\n * @param timeout The maximum time to wait.\n */\n async click(locator: Locator, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n // force: true bypasses Ionic overlay elements (ion-col, ion-toolbar) that intercept pointer events\n await locator.click({ force: true });\n }\n\n /**\n * Fill an input and wait for it to be ready.\n * @param locator The locator to fill.\n * @param value The value to enter.\n * @param timeout The maximum time to wait.\n */\n async fill(locator: Locator, value: string, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n await locator.fill(value);\n }\n\n /**\n * Wait for an element to be visible.\n * @param locator The locator to wait for.\n * @param timeout The maximum time to wait.\n */\n async waitForVisible(locator: Locator, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n }\n\n /**\n * Wait for an element to be hidden.\n * @param locator The locator to wait for.\n * @param timeout The maximum time to wait.\n */\n async waitForHidden(locator: Locator, timeout = 15_000): Promise<void> {\n await locator.waitFor({ state: 'hidden', timeout });\n }\n\n /* --- Unified Selectors --- */\n\n /**\n * Get a button by its text content.\n * Supports both Ionic and Material buttons.\n * @param text The text content to match.\n * @param scope Optional locator to scope the search within.\n */\n button(text: string | RegExp, scope?: Locator): Locator {\n // Combine selectors for both Ionic and Material buttons\n const selectors = [\n IonicSelectors.button,\n 'button[mat-button]',\n 'button[mat-raised-button]',\n 'button[mat-flat-button]',\n 'button[mat-stroked-button]',\n 'button[mat-icon-button]',\n 'button[mat-fab]',\n 'button[mat-mini-fab]',\n ].join(', ');\n\n const root = scope ?? this.page;\n return root.locator(selectors, { hasText: text });\n }\n\n /**\n * Get an icon button by its icon name.\n * Supports both Ionic and Material icon buttons.\n * @param iconName The icon name (e.g. 'mail', 'search', 'ellipsis-horizontal').\n * @param scope Optional locator to scope the search within.\n */\n iconButton(iconName: string, scope?: Locator): Locator {\n const matIconSelector = `${MaterialSelectors.icon}:has-text(\"${iconName}\")`;\n const ionIconSelector = `ion-icon[name=\"${iconName}\"]`;\n const root = scope ?? this.page;\n return root.locator(`button, ion-button`).filter({\n has: this.page.locator(`${matIconSelector}, ${ionIconSelector}`),\n });\n }\n\n /**\n * Access to the underlying Playwright Page object.\n */\n get pageObj(): Page {\n return this.page;\n }\n\n /** Get a modal by its selector */\n modal(selector: string): Locator {\n return this.ionic.modal(selector);\n }\n\n /** Get a tab by its label text */\n tab(text: string | RegExp): Locator {\n return this.mat.tabByLabel(text);\n }\n\n /* --- Specialized Helpers (delegated) --- */\n\n /** Wait for the app to be ready */\n async waitForAppReady(timeout = 15_000): Promise<void> {\n await this.ionic.waitForAppReady(timeout);\n }\n\n /** Wait for loading spinners to disappear */\n async waitForLoadingComplete(timeout = 15_000): Promise<void> {\n await this.ionic.waitForLoadingComplete(timeout);\n }\n\n /** Wait for table rows to be loaded, optionally scoped to a parent locator */\n async waitForTableRows(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.mat.waitForTableRows(timeout, scope);\n }\n\n /** Get a single table row by index (0-based), optionally scoped to a parent locator */\n tableRow(index: number, scope?: Locator): Locator {\n return this.mat.tableRow(index, scope);\n }\n\n /** Click on a table row by index, optionally scoped to a parent locator */\n async clickRow(index: number, scope?: Locator): Promise<void> {\n await this.click(this.tableRow(index, scope));\n }\n\n /** Get the cells of a table column, optionally scoped to a parent locator */\n tableCells(column: string, scope?: Locator): Locator {\n return this.mat.tableCells(column, scope);\n }\n\n /** Get the cell of a row, in the given column */\n rowCell(row: Locator, column: string): Locator {\n return this.mat.rowCell(row, column);\n }\n\n /** Get the table rows having a cell (in the given column) that contains the text */\n tableRowsByCell(column: string, text: string | RegExp, scope?: Locator): Locator {\n return this.mat.tableRowsByCell(column, text, scope);\n }\n\n /** Get the header cell of a table column */\n tableHeaderCell(column: string, scope?: Locator): Locator {\n return this.mat.tableHeaderCell(column, scope);\n }\n\n /** Read the total count, from the table paginator */\n async paginatorTotal(scope?: Locator): Promise<number> {\n return this.mat.paginatorTotal(scope);\n }\n\n /** Wait for the progress bar(s) to stop (e.g. end of all HTTP requests) */\n async waitForProgressBarIdle(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.mat.waitForProgressBarIdle(timeout, scope);\n }\n\n /** Fill a mat-form-field input identified by its formControlName */\n async fillFormControl(formControlName: string, value: string): Promise<void> {\n await this.mat.fillFormControl(formControlName, value);\n }\n\n /** Select an autocomplete option */\n async selectAutocompleteOption(formControlName: string, searchText: string, optionText: string | RegExp): Promise<void> {\n await this.mat.selectAutocompleteOption(formControlName, searchText, optionText);\n }\n\n /**\n * Select an option from a mat-select dropdown.\n * Opens the dropdown, waits for options, clicks the matching one, then waits for the dropdown to close.\n */\n async selectOption(formControlName: string, optionText: string | RegExp): Promise<void> {\n await this.mat.selectMatSelectOption(formControlName, optionText);\n }\n}\n","// GPL v3 License\nimport { Page } from '@playwright/test';\nimport { UiHelper } from './ui.utils';\nimport { STARTUP_DATA_STORAGE_KEY } from './startup.constants';\n\n/**\n * Helper for authentication in e2e tests.\n */\nexport class AuthHelper {\n protected ui: UiHelper;\n\n constructor(\n private page: Page,\n ui?: UiHelper\n ) {\n this.ui = ui ?? new UiHelper(page);\n }\n\n /** Login with given test credentials */\n async login(username: string, password: string): Promise<void> {\n const ui = this.ui;\n\n await this.page.goto('/');\n\n // Wait for the app to fully load (backend connection can take time)\n await ui.waitForAppReady(30_000);\n await ui.waitForLoadingComplete(30_000);\n\n // Click the login button on the home page\n const loginButton = ui.button(/Se connecter|Log In/).first();\n await ui.click(loginButton, 30_000);\n\n // Wait for the auth modal to appear\n const modal = ui.modal('app-auth-form');\n await ui.waitForVisible(modal);\n\n // Fill credentials (may or may not be pre-filled depending on the environment)\n const usernameInput = modal.locator('input[formcontrolname=\"username\"]');\n await usernameInput.click({ force: true });\n await usernameInput.fill(username);\n const passwordInput = modal.locator('input[formcontrolname=\"password\"]');\n await passwordInput.click({ force: true });\n await passwordInput.fill(password);\n\n // Click the submit button in the modal footer\n const ionModal = ui.pageObj.locator('ion-modal:has(app-auth-form)');\n const submitButton = ionModal.locator('ion-footer ion-button[color=\"tertiary\"]');\n await submitButton.waitFor({ state: 'visible', timeout: 10_000 });\n // Wait for the button to be enabled (form valid)\n await ui.pageObj.waitForFunction((btn) => !btn.hasAttribute('disabled'), await submitButton.elementHandle(), { timeout: 5_000 });\n await submitButton.click();\n\n // Wait for the modal to close and verify the user is logged in\n await ui.waitForHidden(modal, 30_000);\n\n // After login, the login button should no longer be visible (replaced by user menu)\n await loginButton.waitFor({ state: 'hidden', timeout: 10_000 });\n }\n\n /** Wait until the user is logged in and return the email shown in the side menu */\n async waitForLoggedIn(): Promise<string | null> {\n const emailEl = this.page.locator('ion-menu h4').first();\n await emailEl.waitFor({ state: 'visible', timeout: 30_000 });\n return emailEl.textContent();\n }\n\n /** Setup startup data with provided credentials or environment variables */\n async loginByEnv(opts?: { username?: string; password?: string; offline?: boolean; peerUrl?: string }): Promise<void> {\n const username = opts?.username ?? process.env.E2E_USERNAME;\n const password = opts?.password ?? process.env.E2E_PASSWORD;\n const offline = opts?.offline ?? process.env.E2E_OFFLINE === 'true';\n const peerUrl = opts?.peerUrl ?? process.env.E2E_PEER_URL;\n\n if (!username && !password && !peerUrl) return;\n\n await this.page.addInitScript(\n ({ key, username, password, offline, peerUrl }) => {\n (window as any)[key] = { username, password, offline, peerUrl };\n },\n { key: STARTUP_DATA_STORAGE_KEY, username, password, offline, peerUrl }\n );\n }\n}\n","// GPL v3 License\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { PlaywrightTestArgs, test } from '@playwright/test';\nimport { AuthHelper } from '../utils/auth.utils';\n\n/**\n * Search environments based on provided criteria.\n *\n * Use when you need to run tests with one or more environments.\n * Test can use `testWithEnv` to run tests with a specific environment.\n * Example :\n *\n * ```\n * test.describe('Sample test with environment', () => {\n * withEnvs({\n * peerTags: ['local'],\n * userTags: ['admin'],\n * }).forEach((env) => {\n * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {\n * // Test implementation\n * });\n * });\n * });\n * ```\n *\n * Environments are loaded from a JSON config file located at one of four paths :\n * - `LOGIN_ENVS_PATH` environment variable\n * - `.environments.json` in the current directory (e.g. `.environments.json`)\n * - `.environments.json` in the `.local` directory (e.g. `.local/environments.json`)\n * - `.environments.json` in the `e2e` directory (e.g. `e2e/.environments.json`)\n *\n * Example of JSON :\n *\n * ```\n * {\n * \"peers\": [\n * {\n * \"id\": \"local\",\n * \"tags\": [\"local\"],\n * \"url\": \"http://localhost:8080\",\n * \"users\": [\n * {\n * \"tags\": [\"admin\"],\n * \"username\": \"admin@sumaris.net\",\n * \"password\": \"admin\"\n * }\n * ]\n * }\n * ]\n * }\n * ```\n *\n * @see testWithEnv\n * @param filter Search criteria.\n */\nexport function withEnvs(filter: EnvFilter): LoginEnv[] {\n const startupEnvs: LoginEnv[] = [];\n\n // Search startup env config file\n const candidates = [\n process.env.LOGIN_ENVS_PATH,\n path.resolve(process.cwd(), '.environments.json'),\n path.resolve(process.cwd(), '.local/environments.json'),\n path.resolve(process.cwd(), 'e2e/.environments.json'),\n ].filter(Boolean);\n const environmentsFile = candidates.find((p) => fs.existsSync(p!));\n if (!environmentsFile) {\n throw new Error(`Startup envs config file not found.\\nTried: ${candidates.join('\\n')}`);\n }\n\n // Load startup config file\n let config: TestEnvironments;\n try {\n config = JSON.parse(fs.readFileSync(environmentsFile, 'utf8'));\n } catch (error) {\n console.error('Error parsing startup envs config file:', error);\n throw error;\n } finally {\n fs.closeSync(fs.openSync(environmentsFile, 'r'));\n }\n\n // Filter startup envs based on criteria\n for (const peerConfig of config.peers) {\n const matchPeerId = filter.peerId && peerConfig.id === filter.peerId;\n const matchPeerTag = filter.peerTags && peerConfig.tags.some((tag) => filter.peerTags?.includes(tag));\n if (!matchPeerId && !matchPeerTag) continue;\n\n for (const userConfig of peerConfig.users) {\n const matchUserTag = filter.userTags && userConfig.tags.some((tag) => filter.userTags?.includes(tag));\n if (!matchUserTag) continue;\n\n startupEnvs.push({\n id: peerConfig.id,\n peerUrl: peerConfig.url,\n username: userConfig.username,\n password: userConfig.password,\n });\n }\n }\n\n return startupEnvs;\n}\n\n/**\n * Currying function that returns a test function that can be used to run tests with a specific environment.\n *\n * ```\n * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {\n * // Test implementation\n * })\n * ```\n *\n * @see withEnvs\n * @param env Environment to use for the test.\n */\nexport function testWithEnv(env: LoginEnv) {\n return (title: string, testFn: (args: PlaywrightTestArgs) => Promise<void>) => {\n test(title, async ({ page, context: playwrightContext, browser, request }) => {\n const auth = new AuthHelper(page);\n await auth.loginByEnv(env);\n\n await testFn({ page, context: playwrightContext, browser, request } as any);\n });\n };\n}\n\nexport interface EnvFilter {\n peerId?: string;\n peerTags?: string[];\n userTags?: string[];\n}\n\nexport interface LoginEnv {\n id?: string;\n peerUrl: string;\n username: string;\n password: string;\n}\n\ninterface TestEnvironments {\n peers: TestPeer[];\n}\n\ninterface TestPeer {\n id?: string;\n tags: string[];\n url: string;\n users: TestUser[];\n}\n\ninterface TestUser {\n tags: string[];\n username: string;\n password: string;\n}\n","// constants\nexport * from './utils/startup.constants';\n\n// utils\nexport * from './utils/auth.utils';\nexport * from './utils/ionic.utils';\nexport * from './utils/material.utils';\nexport * from './utils/ui.utils';\n\n// test\nexport * from './test/login-env';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './testing_api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sumaris-net-ngx-components-testing.mjs","sources":["../../testing/utils/startup.constants.ts","../../testing/utils/ionic.utils.ts","../../testing/utils/material.utils.ts","../../testing/utils/ui.utils.ts","../../testing/utils/auth.utils.ts","../../testing/utils/option-padding.utils.ts","../../testing/test/login-env.ts","../../testing/testing_api.ts","../../testing/sumaris-net-ngx-components-testing.ts"],"sourcesContent":["// Standalone startup constants for the testing package.\n// Must be kept in sync with src/app/core/services/startup.constants.ts\nexport const STARTUP_DATA_STORAGE_KEY = '__appStartupData';\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\n\n/**\n * Utility selectors and helpers for Ionic components in e2e tests.\n */\nexport const IonicSelectors = {\n /** Ionic button */\n button: 'ion-button',\n /** Ionic modal */\n modal: 'ion-modal',\n /** Ionic content area */\n content: 'ion-content',\n /** Ionic item */\n item: 'ion-item',\n /** Ionic label */\n label: 'ion-label',\n /** Ionic spinner */\n spinner: 'ion-spinner',\n /** Ionic skeleton text (loading placeholder) */\n skeletonText: 'ion-skeleton-text',\n /** Ionic back button */\n backButton: 'ion-back-button',\n /** Ionic toolbar */\n toolbar: 'ion-toolbar',\n /** Ionic footer */\n footer: 'ion-footer',\n /** Ionic header */\n header: 'ion-header',\n};\n\n/**\n * Helper functions for interacting with Ionic components.\n */\nexport class IonicHelper {\n constructor(private page: Page) {}\n\n /** Get an Ionic button by text content */\n buttonByText(text: string | RegExp): Locator {\n return this.page.locator(IonicSelectors.button, { hasText: text });\n }\n\n /** Get an Ionic button by CSS selector */\n button(selector?: string, options?: { hasText?: string | RegExp }): Locator {\n const fullSelector = selector ? `${IonicSelectors.button}${selector}` : IonicSelectors.button;\n return this.page.locator(fullSelector, options);\n }\n\n /**\n * Get an Ionic modal by its inner component selector (e.g. 'app-auth-form').\n * Automatically wraps the selector in `ion-modal:has(...)` so callers\n * only need to provide the component tag name.\n */\n modal(selector: string): Locator {\n const fullSelector = selector.startsWith('ion-modal') ? selector : `ion-modal:has(${selector})`;\n return this.page.locator(fullSelector);\n }\n\n /** Wait for the Ionic app to be ready */\n async waitForAppReady(timeout = 15_000): Promise<void> {\n // Wait for ion-app to be attached (Angular bootstrap complete)\n await this.page.waitForSelector('ion-app', { state: 'attached', timeout });\n }\n\n /** Wait for loading spinners to disappear */\n async waitForLoadingComplete(timeout = 15_000): Promise<void> {\n await this.page\n .locator(IonicSelectors.spinner)\n .waitFor({ state: 'hidden', timeout })\n .catch(() => {\n // Spinner may not appear at all — that's fine\n });\n }\n}\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\n\n/**\n * Utility selectors and helpers for Angular Material components in e2e tests.\n */\nexport const MaterialSelectors = {\n /** Material table row */\n row: 'mat-row, tr[mat-row]',\n /** Material header row */\n headerRow: 'mat-header-row, tr[mat-header-row]',\n /** Material cell */\n cell: 'mat-cell, td[mat-cell]',\n /** Material form field */\n formField: 'mat-form-field',\n /** Material input */\n input: 'input[matInput]',\n /** Material tab group */\n tabGroup: 'mat-tab-group',\n /** Material tab label */\n tabLabel: '.mat-mdc-tab',\n /** Material tab link (tab nav bar) */\n tabLink: '.mat-mdc-tab-link',\n /** Material paginator */\n paginator: 'mat-paginator',\n /** Material paginator range label (e.g. '1 - 20 of 42') */\n paginatorRangeLabel: '.mat-mdc-paginator-range-label',\n /** Material progress bar */\n progressBar: 'mat-progress-bar',\n /** Material icon */\n icon: 'mat-icon',\n /** Material autocomplete panel */\n autocompletePanel: '.mat-mdc-autocomplete-panel',\n /** Material autocomplete option */\n autocompleteOption: 'mat-option',\n /** Material autocomplete trigger (input) */\n autocompleteTrigger: 'input.mat-mdc-autocomplete-trigger',\n /** Material select panel */\n selectPanel: '.mat-mdc-select-panel',\n /** Material select trigger */\n selectTrigger: '.mat-mdc-select-trigger',\n};\n\n/**\n * Helper functions for interacting with Angular Material components.\n */\nexport class MaterialHelper {\n constructor(private page: Page) {}\n\n /** Append a suffix (e.g. a class) to each part of a selector list (e.g. 'mat-cell, td[mat-cell]') */\n protected static withSuffix(selectors: string, suffix: string): string {\n return selectors\n .split(',')\n .map((selector) => selector.trim() + suffix)\n .join(', ');\n }\n\n /** Get all visible table rows, optionally scoped to a parent locator */\n tableRows(scope?: Locator): Locator {\n const root = scope ?? this.page;\n return root.locator(MaterialSelectors.row);\n }\n\n /** Get a single table row by index (0-based), optionally scoped to a parent locator */\n tableRow(index: number, scope?: Locator): Locator {\n return this.tableRows(scope).nth(index);\n }\n\n /** Click on a table row by index (0-based), optionally scoped to a parent locator */\n async clickRow(index: number, scope?: Locator): Promise<void> {\n await this.tableRow(index, scope).click();\n }\n\n /** Get a tab by its label text */\n tabByLabel(text: string | RegExp): Locator {\n return this.page.locator(MaterialSelectors.tabLabel, { hasText: text });\n }\n\n /** Click a tab by its label text */\n async clickTab(text: string | RegExp): Promise<void> {\n await this.tabByLabel(text).click();\n }\n\n /** Fill a mat-form-field input identified by its formControlName.\n * Supports both native inputs with formcontrolname and custom components (e.g. mat-autocomplete-field) wrapping an input. */\n async fillFormControl(formControlName: string, value: string): Promise<void> {\n // Try native input first, then fall back to custom component wrapper\n const nativeInput = this.page.locator(`${MaterialSelectors.formField} input[formcontrolname=\"${formControlName}\"]`);\n const wrappedInput = this.page.locator(`[formcontrolname=\"${formControlName}\"] input`);\n const input = (await nativeInput.count()) > 0 ? nativeInput : wrappedInput;\n // Click the parent mat-form-field to ensure focus reaches the input\n const formField = input.locator('xpath=ancestor::mat-form-field');\n if ((await formField.count()) > 0) {\n await formField.first().click();\n } else {\n await input.click({ force: true });\n }\n await input.pressSequentially(value, { delay: 50 });\n }\n\n /**\n * Select an option from a mat-select dropdown by formControlName.\n * Opens the dropdown, waits for options to render, clicks the matching option, then waits for the dropdown to close.\n */\n async selectMatSelectOption(formControlName: string, optionText: string | RegExp): Promise<void> {\n const select = this.page.locator(`mat-select[formcontrolname=\"${formControlName}\"]`);\n await select.click();\n // Use role-based selector: mat-select renders options with role=\"option\" in a listbox\n const option = this.page.getByRole('option', { name: optionText });\n await option.waitFor({ state: 'visible', timeout: 10_000 });\n await option.click();\n // Wait for the option to disappear (dropdown closed)\n await option.waitFor({ state: 'hidden', timeout: 5_000 });\n }\n\n /** Select an autocomplete option by text after typing in a form control */\n async selectAutocompleteOption(formControlName: string, searchText: string, optionText: string | RegExp): Promise<void> {\n await this.fillFormControl(formControlName, searchText);\n const panel = this.page.locator(MaterialSelectors.autocompletePanel);\n await panel.waitFor({ state: 'visible', timeout: 10_000 });\n await panel.locator(MaterialSelectors.autocompleteOption, { hasText: optionText }).click();\n }\n\n /** Get the cells of a table column (e.g. 'lastName'), optionally scoped to a parent locator */\n tableCells(column: string, scope?: Locator): Locator {\n return this.tableRows(scope).locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`));\n }\n\n /** Get the cell of a row, in the given column (e.g. 'lastName') */\n rowCell(row: Locator, column: string): Locator {\n return row.locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`));\n }\n\n /** Get the table rows having a cell (in the given column) that contains the text, optionally scoped to a parent locator */\n tableRowsByCell(column: string, text: string | RegExp, scope?: Locator): Locator {\n return this.tableRows(scope).filter({\n has: this.page.locator(MaterialHelper.withSuffix(MaterialSelectors.cell, `.mat-column-${column}`), { hasText: text }),\n });\n }\n\n /** Get the header cell of a table column (e.g. to sort it), optionally scoped to a parent locator */\n tableHeaderCell(column: string, scope?: Locator): Locator {\n const root = scope ?? this.page;\n return root.locator(MaterialHelper.withSuffix(MaterialSelectors.headerRow, ` .mat-column-${column}`));\n }\n\n /** Read the total count, from the paginator range label (e.g. '1 - 20 of 42' -> 42), optionally scoped to a parent locator */\n async paginatorTotal(scope?: Locator): Promise<number> {\n const root = scope ?? this.page;\n const label = root.locator(`${MaterialSelectors.paginator} ${MaterialSelectors.paginatorRangeLabel}`).first();\n await label.waitFor({ state: 'visible', timeout: 10_000 });\n const text = ((await label.textContent()) ?? '').trim();\n const total = Number(text.split(/\\s+/).pop());\n return isNaN(total) ? 0 : total;\n }\n\n /** Wait for the progress bar(s) to stop (not in 'query' mode), optionally scoped to a parent locator */\n async waitForProgressBarIdle(timeout = 15_000, scope?: Locator): Promise<void> {\n const root = scope ?? this.page;\n await root.locator(`${MaterialSelectors.progressBar}[mode=\"query\"]`).first().waitFor({ state: 'detached', timeout });\n }\n\n /** Wait for table rows to be loaded (at least one row visible), optionally scoped to a parent locator */\n async waitForTableRows(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.tableRows(scope).first().waitFor({ state: 'visible', timeout });\n }\n}\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\nimport { IonicHelper, IonicSelectors } from './ionic.utils';\nimport { MaterialHelper, MaterialSelectors } from './material.utils';\n\n/**\n * Common UI helper functions to improve readability of e2e tests.\n * This helper centralizes access to Ionic and Material specific helpers.\n */\nexport class UiHelper {\n protected ionic: IonicHelper;\n protected mat: MaterialHelper;\n\n constructor(protected page: Page) {\n this.ionic = new IonicHelper(page);\n this.mat = new MaterialHelper(page);\n }\n\n /**\n * Click an element and wait for it to be ready.\n * @param locator The locator to click.\n * @param timeout The maximum time to wait.\n */\n async click(locator: Locator, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n // force: true bypasses Ionic overlay elements (ion-col, ion-toolbar) that intercept pointer events\n await locator.click({ force: true });\n }\n\n /**\n * Fill an input and wait for it to be ready.\n * @param locator The locator to fill.\n * @param value The value to enter.\n * @param timeout The maximum time to wait.\n */\n async fill(locator: Locator, value: string, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n await locator.fill(value);\n }\n\n /**\n * Wait for an element to be visible.\n * @param locator The locator to wait for.\n * @param timeout The maximum time to wait.\n */\n async waitForVisible(locator: Locator, timeout = 10_000): Promise<void> {\n await locator.waitFor({ state: 'visible', timeout });\n }\n\n /**\n * Wait for an element to be hidden.\n * @param locator The locator to wait for.\n * @param timeout The maximum time to wait.\n */\n async waitForHidden(locator: Locator, timeout = 15_000): Promise<void> {\n await locator.waitFor({ state: 'hidden', timeout });\n }\n\n /* --- Unified Selectors --- */\n\n /**\n * Get a button by its text content.\n * Supports both Ionic and Material buttons.\n * @param text The text content to match.\n * @param scope Optional locator to scope the search within.\n */\n button(text: string | RegExp, scope?: Locator): Locator {\n // Combine selectors for both Ionic and Material buttons\n const selectors = [\n IonicSelectors.button,\n 'button[mat-button]',\n 'button[mat-raised-button]',\n 'button[mat-flat-button]',\n 'button[mat-stroked-button]',\n 'button[mat-icon-button]',\n 'button[mat-fab]',\n 'button[mat-mini-fab]',\n ].join(', ');\n\n const root = scope ?? this.page;\n return root.locator(selectors, { hasText: text });\n }\n\n /**\n * Get an icon button by its icon name.\n * Supports both Ionic and Material icon buttons.\n * @param iconName The icon name (e.g. 'mail', 'search', 'ellipsis-horizontal').\n * @param scope Optional locator to scope the search within.\n */\n iconButton(iconName: string, scope?: Locator): Locator {\n const matIconSelector = `${MaterialSelectors.icon}:has-text(\"${iconName}\")`;\n const ionIconSelector = `ion-icon[name=\"${iconName}\"]`;\n const root = scope ?? this.page;\n return root.locator(`button, ion-button`).filter({\n has: this.page.locator(`${matIconSelector}, ${ionIconSelector}`),\n });\n }\n\n /**\n * Access to the underlying Playwright Page object.\n */\n get pageObj(): Page {\n return this.page;\n }\n\n /** Get a modal by its selector */\n modal(selector: string): Locator {\n return this.ionic.modal(selector);\n }\n\n /** Get a tab by its label text */\n tab(text: string | RegExp): Locator {\n return this.mat.tabByLabel(text);\n }\n\n /* --- Specialized Helpers (delegated) --- */\n\n /** Wait for the app to be ready */\n async waitForAppReady(timeout = 15_000): Promise<void> {\n await this.ionic.waitForAppReady(timeout);\n }\n\n /** Wait for loading spinners to disappear */\n async waitForLoadingComplete(timeout = 15_000): Promise<void> {\n await this.ionic.waitForLoadingComplete(timeout);\n }\n\n /** Wait for table rows to be loaded, optionally scoped to a parent locator */\n async waitForTableRows(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.mat.waitForTableRows(timeout, scope);\n }\n\n /** Get a single table row by index (0-based), optionally scoped to a parent locator */\n tableRow(index: number, scope?: Locator): Locator {\n return this.mat.tableRow(index, scope);\n }\n\n /** Click on a table row by index, optionally scoped to a parent locator */\n async clickRow(index: number, scope?: Locator): Promise<void> {\n await this.click(this.tableRow(index, scope));\n }\n\n /** Get the cells of a table column, optionally scoped to a parent locator */\n tableCells(column: string, scope?: Locator): Locator {\n return this.mat.tableCells(column, scope);\n }\n\n /** Get the cell of a row, in the given column */\n rowCell(row: Locator, column: string): Locator {\n return this.mat.rowCell(row, column);\n }\n\n /** Get the table rows having a cell (in the given column) that contains the text */\n tableRowsByCell(column: string, text: string | RegExp, scope?: Locator): Locator {\n return this.mat.tableRowsByCell(column, text, scope);\n }\n\n /** Get the header cell of a table column */\n tableHeaderCell(column: string, scope?: Locator): Locator {\n return this.mat.tableHeaderCell(column, scope);\n }\n\n /** Read the total count, from the table paginator */\n async paginatorTotal(scope?: Locator): Promise<number> {\n return this.mat.paginatorTotal(scope);\n }\n\n /** Wait for the progress bar(s) to stop (e.g. end of all HTTP requests) */\n async waitForProgressBarIdle(timeout = 15_000, scope?: Locator): Promise<void> {\n await this.mat.waitForProgressBarIdle(timeout, scope);\n }\n\n /** Fill a mat-form-field input identified by its formControlName */\n async fillFormControl(formControlName: string, value: string): Promise<void> {\n await this.mat.fillFormControl(formControlName, value);\n }\n\n /** Select an autocomplete option */\n async selectAutocompleteOption(formControlName: string, searchText: string, optionText: string | RegExp): Promise<void> {\n await this.mat.selectAutocompleteOption(formControlName, searchText, optionText);\n }\n\n /**\n * Select an option from a mat-select dropdown.\n * Opens the dropdown, waits for options, clicks the matching one, then waits for the dropdown to close.\n */\n async selectOption(formControlName: string, optionText: string | RegExp): Promise<void> {\n await this.mat.selectMatSelectOption(formControlName, optionText);\n }\n}\n","// GPL v3 License\nimport { Page } from '@playwright/test';\nimport { UiHelper } from './ui.utils';\nimport { STARTUP_DATA_STORAGE_KEY } from './startup.constants';\n\n/**\n * Helper for authentication in e2e tests.\n */\nexport class AuthHelper {\n protected ui: UiHelper;\n\n constructor(\n private page: Page,\n ui?: UiHelper\n ) {\n this.ui = ui ?? new UiHelper(page);\n }\n\n /** Login with given test credentials */\n async login(username: string, password: string): Promise<void> {\n const ui = this.ui;\n\n await this.page.goto('/');\n\n // Wait for the app to fully load (backend connection can take time)\n await ui.waitForAppReady(30_000);\n await ui.waitForLoadingComplete(30_000);\n\n // Click the login button on the home page\n const loginButton = ui.button(/Se connecter|Log In/).first();\n await ui.click(loginButton, 30_000);\n\n // Wait for the auth modal to appear\n const modal = ui.modal('app-auth-form');\n await ui.waitForVisible(modal);\n\n // Fill credentials (may or may not be pre-filled depending on the environment)\n const usernameInput = modal.locator('input[formcontrolname=\"username\"]');\n await usernameInput.click({ force: true });\n await usernameInput.fill(username);\n const passwordInput = modal.locator('input[formcontrolname=\"password\"]');\n await passwordInput.click({ force: true });\n await passwordInput.fill(password);\n\n // Click the submit button in the modal footer\n const ionModal = ui.pageObj.locator('ion-modal:has(app-auth-form)');\n const submitButton = ionModal.locator('ion-footer ion-button[color=\"tertiary\"]');\n await submitButton.waitFor({ state: 'visible', timeout: 10_000 });\n // Wait for the button to be enabled (form valid)\n await ui.pageObj.waitForFunction((btn) => !btn.hasAttribute('disabled'), await submitButton.elementHandle(), { timeout: 5_000 });\n await submitButton.click();\n\n // Wait for the modal to close and verify the user is logged in\n await ui.waitForHidden(modal, 30_000);\n\n // After login, the login button should no longer be visible (replaced by user menu)\n await loginButton.waitFor({ state: 'hidden', timeout: 10_000 });\n }\n\n /** Wait until the user is logged in and return the email shown in the side menu */\n async waitForLoggedIn(): Promise<string | null> {\n const emailEl = this.page.locator('ion-menu h4').first();\n await emailEl.waitFor({ state: 'visible', timeout: 30_000 });\n return emailEl.textContent();\n }\n\n /** Setup startup data with provided credentials or environment variables */\n async loginByEnv(opts?: { username?: string; password?: string; offline?: boolean; peerUrl?: string }): Promise<void> {\n const username = opts?.username ?? process.env.E2E_USERNAME;\n const password = opts?.password ?? process.env.E2E_PASSWORD;\n const offline = opts?.offline ?? process.env.E2E_OFFLINE === 'true';\n const peerUrl = opts?.peerUrl ?? process.env.E2E_PEER_URL;\n\n if (!username && !password && !peerUrl) return;\n\n await this.page.addInitScript(\n ({ key, username, password, offline, peerUrl }) => {\n (window as any)[key] = { username, password, offline, peerUrl };\n },\n { key: STARTUP_DATA_STORAGE_KEY, username, password, offline, peerUrl }\n );\n }\n}\n","// GPL v3 License\nimport { Locator, Page } from '@playwright/test';\nimport { IonicSelectors } from './ionic.utils';\nimport { MaterialSelectors } from './material.utils';\n\nexport interface OptionPadding {\n left: string;\n right: string;\n}\n\n/** What to expect from options. An application that changes their padding must set the value it expects. */\nexport interface OptionPaddingOptions {\n /** Expected padding of options, unless marked as \"no padding\" */\n padding?: OptionPadding;\n /** Selector of options without padding, managing their own inset (e.g. autocomplete columns, through the `ion-col` padding) */\n noPaddingSelector?: string;\n}\n\n/** Library defaults */\nconst DEFAULT_OPTIONS: Required<OptionPaddingOptions> = {\n padding: { left: '16px', right: '16px' },\n noPaddingSelector: '.ion-no-padding, [no-padding], .mat-autocomplete-searchbar',\n};\n\nconst NO_PADDING: OptionPadding = { left: '0px', right: '0px' };\n\nconst TAB = `${MaterialSelectors.tabLink}:not(.mat-mdc-tab-disabled)`;\nconst ACTIVE_TAB = '.mdc-tab--active';\nconst FIELD_TRIGGER = [\n `${MaterialSelectors.autocompleteTrigger}:not([disabled]):not([readonly])`,\n `mat-select:not(.mat-mdc-select-disabled) ${MaterialSelectors.selectTrigger}`,\n].join(', ');\nconst OPEN_PANEL = `${MaterialSelectors.autocompletePanel}, ${MaterialSelectors.selectPanel}`;\nconst LOADED_OPTION = `${MaterialSelectors.autocompleteOption}:not(:has(${IonicSelectors.skeletonText}))`;\n\nconst PANEL_TIMEOUT = 3_000;\n\nexport interface OptionPaddingReport {\n fieldCount: number;\n optionCount: number;\n violations: string[];\n}\n\n/**\n * Check the padding of `mat-option`, on any page.\n *\n * Nothing here depends on a given field: tabs, fields and options are discovered from the DOM,\n * using the CSS classes set by Angular Material. Test cases can be added to (or removed from)\n * a page without changing the tests.\n */\nexport class OptionPaddingHelper {\n private readonly options: Required<OptionPaddingOptions>;\n\n constructor(\n private page: Page,\n options?: OptionPaddingOptions\n ) {\n this.options = { ...DEFAULT_OPTIONS, ...options };\n }\n\n /** Wait for the page to display at least one field */\n async waitForFields(timeout = 30_000): Promise<void> {\n await this.page.locator(FIELD_TRIGGER).first().waitFor({ state: 'attached', timeout });\n }\n\n /** Open the panel of every field of every tab of the current page, and check all of its options */\n async checkAllFields(): Promise<OptionPaddingReport> {\n const report: OptionPaddingReport = { fieldCount: 0, optionCount: 0, violations: [] };\n const tabs = await this.page.locator(TAB).all();\n\n // Pages without tabs have a single \"anonymous\" view\n for (const tab of tabs.length ? tabs : [null]) {\n if (tab) {\n await tab.click();\n await tab.and(this.page.locator(ACTIVE_TAB)).waitFor();\n }\n const tabName = tab ? (await tab.innerText()).trim() : '';\n await this.checkFieldsOfCurrentView(tabName, report);\n }\n return report;\n }\n\n /**\n * Check the option padding without any field, using fake options added to the page. Their parent is a panel\n * inheriting from `ion-no-padding`, like an inline panel inside a form grid. This catches a global rule leaking\n * from any component loaded by the page (e.g. `::ng-deep .mat-mdc-option`), or padding vars inherited from a parent.\n */\n async checkProbes(): Promise<string[]> {\n const violations: string[] = [];\n\n for (const noPadding of [true, false]) {\n const { left, right } = await this.page.evaluate((isNoPadding) => {\n const parent = document.createElement('div');\n parent.className = `ion-no-padding ${isNoPadding ? 'mat-mdc-autocomplete-panel' : 'mat-mdc-select-panel'}`;\n const option = parent.appendChild(document.createElement('mat-option'));\n option.className = isNoPadding ? 'mat-mdc-option ion-no-padding' : 'mat-mdc-option';\n document.body.appendChild(parent);\n const { paddingLeft, paddingRight } = getComputedStyle(option);\n parent.remove();\n return { left: paddingLeft, right: paddingRight };\n }, noPadding);\n\n const violation = this.checkPadding('Probe option', left, right, noPadding);\n if (violation) violations.push(violation);\n }\n return violations;\n }\n\n protected async checkFieldsOfCurrentView(tabName: string, report: OptionPaddingReport): Promise<void> {\n const triggers = await this.page.locator(FIELD_TRIGGER).filter({ visible: true }).all();\n\n for (const trigger of triggers) {\n report.fieldCount++;\n const field = `[${tabName}] \"${await this.getFieldName(trigger)}\"`;\n\n const panel = await this.openPanel(trigger);\n if (!panel) continue; // The field has no panel to show (e.g. no item)\n\n for (const { text, noPadding, left, right } of await this.readOptions(panel)) {\n report.optionCount++;\n const violation = this.checkPadding(`${field} > option \"${text}\"`, left, right, noPadding);\n if (violation) report.violations.push(violation);\n }\n await this.closePanel(panel);\n }\n }\n\n protected checkPadding(where: string, left: string, right: string, noPadding: boolean): string | null {\n const expected = noPadding ? NO_PADDING : this.options.padding;\n return left === expected.left && right === expected.right\n ? null\n : `${where}: padding left/right is ${left}/${right}, expected ${expected.left}/${expected.right} (${noPadding ? 'no-padding option' : 'option with padding'})`;\n }\n\n protected getFieldName(trigger: Locator): Promise<string> {\n return trigger.evaluate((el) => {\n const label = el.closest('mat-form-field')?.querySelector('mat-label')?.textContent;\n const name = label || el.getAttribute('placeholder') || el.closest('mat-select')?.getAttribute('aria-label');\n return name?.trim() || el.tagName.toLowerCase();\n });\n }\n\n /** Open the field's panel. Dispatch the event, because some triggers ignore the pointer until focused (e.g. lat/long sign) */\n protected async openPanel(trigger: Locator): Promise<Locator | null> {\n await trigger.scrollIntoViewIfNeeded();\n await trigger.focus();\n await trigger.dispatchEvent('click');\n\n const panel = this.page.locator(OPEN_PANEL).filter({ visible: true }).first();\n try {\n await panel.locator(LOADED_OPTION).first().waitFor({ state: 'attached', timeout: PANEL_TIMEOUT });\n return panel;\n } catch {\n return null;\n }\n }\n\n protected async closePanel(panel: Locator): Promise<void> {\n await this.page.keyboard.press('Escape');\n await panel.waitFor({ state: 'hidden', timeout: PANEL_TIMEOUT });\n }\n\n protected readOptions(panel: Locator) {\n return panel.locator(LOADED_OPTION).evaluateAll(\n (options, noPaddingSelector) =>\n options.map((option) => {\n const { paddingLeft, paddingRight } = getComputedStyle(option);\n return {\n text: (option.textContent ?? '').trim().slice(0, 40),\n noPadding: option.matches(noPaddingSelector),\n left: paddingLeft,\n right: paddingRight,\n };\n }),\n this.options.noPaddingSelector\n );\n }\n}\n","// GPL v3 License\nimport * as path from 'node:path';\nimport * as fs from 'node:fs';\nimport { PlaywrightTestArgs, test } from '@playwright/test';\nimport { AuthHelper } from '../utils/auth.utils';\n\n/**\n * Search environments based on provided criteria.\n *\n * Use when you need to run tests with one or more environments.\n * Test can use `testWithEnv` to run tests with a specific environment.\n * Example :\n *\n * ```\n * test.describe('Sample test with environment', () => {\n * withEnvs({\n * peerTags: ['local'],\n * userTags: ['admin'],\n * }).forEach((env) => {\n * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {\n * // Test implementation\n * });\n * });\n * });\n * ```\n *\n * Environments are loaded from a JSON config file located at one of four paths :\n * - `LOGIN_ENVS_PATH` environment variable\n * - `.environments.json` in the current directory (e.g. `.environments.json`)\n * - `.environments.json` in the `.local` directory (e.g. `.local/environments.json`)\n * - `.environments.json` in the `e2e` directory (e.g. `e2e/.environments.json`)\n *\n * Example of JSON :\n *\n * ```\n * {\n * \"peers\": [\n * {\n * \"id\": \"local\",\n * \"tags\": [\"local\"],\n * \"url\": \"http://localhost:8080\",\n * \"users\": [\n * {\n * \"tags\": [\"admin\"],\n * \"username\": \"admin@sumaris.net\",\n * \"password\": \"admin\"\n * }\n * ]\n * }\n * ]\n * }\n * ```\n *\n * @see testWithEnv\n * @param filter Search criteria.\n */\nexport function withEnvs(filter: EnvFilter): LoginEnv[] {\n const startupEnvs: LoginEnv[] = [];\n\n // Search startup env config file\n const candidates = [\n process.env.LOGIN_ENVS_PATH,\n path.resolve(process.cwd(), '.environments.json'),\n path.resolve(process.cwd(), '.local/environments.json'),\n path.resolve(process.cwd(), 'e2e/.environments.json'),\n ].filter(Boolean);\n const environmentsFile = candidates.find((p) => fs.existsSync(p!));\n if (!environmentsFile) {\n throw new Error(`Startup envs config file not found.\\nTried: ${candidates.join('\\n')}`);\n }\n\n // Load startup config file\n let config: TestEnvironments;\n try {\n config = JSON.parse(fs.readFileSync(environmentsFile, 'utf8'));\n } catch (error) {\n console.error('Error parsing startup envs config file:', error);\n throw error;\n } finally {\n fs.closeSync(fs.openSync(environmentsFile, 'r'));\n }\n\n // Filter startup envs based on criteria\n for (const peerConfig of config.peers) {\n const matchPeerId = filter.peerId && peerConfig.id === filter.peerId;\n const matchPeerTag = filter.peerTags && peerConfig.tags.some((tag) => filter.peerTags?.includes(tag));\n if (!matchPeerId && !matchPeerTag) continue;\n\n for (const userConfig of peerConfig.users) {\n const matchUserTag = filter.userTags && userConfig.tags.some((tag) => filter.userTags?.includes(tag));\n if (!matchUserTag) continue;\n\n startupEnvs.push({\n id: peerConfig.id,\n peerUrl: peerConfig.url,\n username: userConfig.username,\n password: userConfig.password,\n });\n }\n }\n\n return startupEnvs;\n}\n\n/**\n * Currying function that returns a test function that can be used to run tests with a specific environment.\n *\n * ```\n * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {\n * // Test implementation\n * })\n * ```\n *\n * @see withEnvs\n * @param env Environment to use for the test.\n */\nexport function testWithEnv(env: LoginEnv) {\n return (title: string, testFn: (args: PlaywrightTestArgs) => Promise<void>) => {\n test(title, async ({ page, context: playwrightContext, browser, request }) => {\n const auth = new AuthHelper(page);\n await auth.loginByEnv(env);\n\n await testFn({ page, context: playwrightContext, browser, request } as any);\n });\n };\n}\n\nexport interface EnvFilter {\n peerId?: string;\n peerTags?: string[];\n userTags?: string[];\n}\n\nexport interface LoginEnv {\n id?: string;\n peerUrl: string;\n username: string;\n password: string;\n}\n\ninterface TestEnvironments {\n peers: TestPeer[];\n}\n\ninterface TestPeer {\n id?: string;\n tags: string[];\n url: string;\n users: TestUser[];\n}\n\ninterface TestUser {\n tags: string[];\n username: string;\n password: string;\n}\n","// constants\nexport * from './utils/startup.constants';\n\n// utils\nexport * from './utils/auth.utils';\nexport * from './utils/ionic.utils';\nexport * from './utils/material.utils';\nexport * from './utils/option-padding.utils';\nexport * from './utils/ui.utils';\n\n// test\nexport * from './test/login-env';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './testing_api';\n"],"names":[],"mappings":"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"}