@sumaris-net/ngx-components 21.0.0-rc2 → 21.0.0-rc20

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
@@ -1,6 +1,5 @@
1
1
  # Change log
2
2
 
3
-
4
3
  ## 0.1.7
5
4
 
6
5
  - enh: Menu: Allow dynamic insertion, using a new config options (see 'CORE_CONFIG_OPTIONS.MENU_ITEMS')
@@ -2224,3 +2223,92 @@ enh: Environment: add useHash property to configure Angular router to use hash U
2224
2223
 
2225
2224
  ## 18.24.7
2226
2225
  - fix(menu): fix color in svg icon
2226
+
2227
+ ## 18.24.9
2228
+ - fix(platform) Fix version.appup parsing (avoid infinite loop when on '-alpha' versions)
2229
+ - enh(upload-popover) Allow overriding buttons text
2230
+
2231
+ ## 18.24.12
2232
+ - enh(images.utils) Add `ImagesUtils.writeBase64ToFile`
2233
+
2234
+ ## 18.24.14
2235
+ - enh(form.pipes): Add new pipes for accessing FormArray controls and groups
2236
+ - fix(dates) Fix fromDateISOString() when requiredTime and no millisecond given
2237
+ - test(validators): Add unit tests for `validDate` and `validDateAllowNoTime` validation methods
2238
+
2239
+ ## 18.24.18
2240
+ - fix(tab-editor) Fix event argument typing, in `AppTabEditor.onSwipeTab()`
2241
+
2242
+ ## 18.24.21
2243
+ - enh(table) Allow to define reserved columns (start and end) in `AsyncTable`
2244
+
2245
+ ## 18.24.22
2246
+ - fix(menu) Allow to hide the footer, on small screens
2247
+
2248
+ ## 18.24.23
2249
+ - fix(material.date) Prevent event propagation in `openDatePicker()`
2250
+
2251
+ ## 18.24.24
2252
+ - enh(FileUtils) Add more options to `showUploadPopover()` like `backdropDismiss`, `keyboardClose` and `translucent`.
2253
+
2254
+ ## 18.24.25
2255
+ - fix(platform) Let Capacitor config handle accessory bar
2256
+
2257
+ ## 18.25.0
2258
+ - enh(startup) Add a startup service to be able to automatically set the peer and authenticate user
2259
+ - enh(testing) Add a testing package for shared testing utilities
2260
+
2261
+ ## 18.25.1
2262
+ - fix(startup) Missing export symbols in public API
2263
+
2264
+ ## 18.25.3
2265
+ - fix(animations) Fix Chrome warning about non-animatable property `overflow` in `expansionAnimation`
2266
+
2267
+ ## 18.25.4
2268
+ - fix(AppAsyncTable) Add `logPrefix` property to allow customizing the logs prefix
2269
+
2270
+ ## 18.25.5
2271
+ - enh(mat-autocomplete-field) Add custom header and footer support for `mat-autocomplete-field`, using `<ng-template panelHeader>` and `<ng-template panelFooter>`
2272
+ - enh(mat-autocomplete-field) Add new inputs `showPanelHeader` and `showPanelFooter` to hide defaults header/footer
2273
+
2274
+ ## 18.25.6
2275
+ - enh(gallery) Add `zoomPresentation` to display slideshow without modal, but inside the main container
2276
+
2277
+ ## 18.25.15
2278
+ - enh(gallery) Rename mouse zoom, rotate and crop options to `enableMouseZoom`, `enableRotate` and `enableCrop`
2279
+ - enh(gallery) Rename `zoomPresentation` into `slideshowMode`
2280
+
2281
+ ## 18.26.0
2282
+ - enh(icon-selector) New component suite for icon selection: `AppIconSelectorModal` (modal with tabs, search, badges) and `AppIconSelectorField` (form field with ControlValueAccessor)
2283
+ - enh(generate-icons) New `npm run generate:icons` script to generate `assets/icons-ref.json` from `node_modules` (ionicons + material-design-icons)
2284
+
2285
+ ## 21.0.0
2286
+ - enh(core) Migration to Angular 21 and Ionic 8
2287
+ - enh(e2e) Allow to run e2e on many peer/login, using file `e2e/.environments.json` or `.environments.json` (or `.local/environments.json`)
2288
+ - fix(Toolbar) Add subTitle input
2289
+ - fix(AuthForm): fix error message display
2290
+ - fix(UsersTable): fix error message display and use @if instead of @ngIf
2291
+ - enh(autocomplete): Add `showHeaderList` and `showFooterList` inputs to control header/footer visibility in dropdown
2292
+ - enh(autocomplete): Add `headerListTemplate` and `footerListTemplate` content projection slots for custom header/footer content
2293
+ - enh(about) Add changelog link (Notes de version) to About modal, using ConfigurationService and markdown modal
2294
+ - Breaking change: GraphqlService now driven by `APP_GRAPHQL_SERVICE` token, and provided by default with `CoreModule.forRoot()`; Removed from service constructors
2295
+ - Breaking change: Translate service updated to v18: the TranslateModule has been removed. Now use `provideTranslateService()` or `provideChildTranslateService()`
2296
+ - enh(GraphqlService): add `onExpiredAuth` subject to handle expired auth events, registered by account service.
2297
+ - enh(deps): apollo-link-queue and apollo-link-serialize deps removed. Replace by specific implementations
2298
+ - fix(ui): `ion-label` no longer supports `[innerHTML]` bindings (use text interpolation or `ion-text` instead)
2299
+ - fix(settings): prevent default event behavior in `showSelectPeerModal` (avoid the keyboard to appear, on mobile)
2300
+ - fix(android): fix Android build (upgrade Gradle wrapper to 8.14.5) and target SDK API 36
2301
+ - fix(android): fix Android keyboard issue (sumaris-app#2076) and refine EdgeToEdge / StatusBar compatibility for Android 15-17
2302
+ - fix(theme): add padding-inline fallback for Chrome < 87 compatibility
2303
+ - fix(app): fallback to `provideNoopAnimations()` if Web Animations API is unsupported
2304
+ - fix(utils:compat): add core-js compat imports and browserslist configuration
2305
+ - fix(core): ensure toast service waits for initialization before displaying
2306
+ - enh(about): generate changelog from GitLab release API
2307
+ - fix(table): waitWhilePending timeout handling in confirmEdit/confirmCreate (Mantis 69886)
2308
+ - enh(alerts): implement and expose options (`backdropDismiss`, `keyboardClose`) to callers
2309
+ - enh(upload-file): add check for maximum file size and blacklisted extensions
2310
+ - test(markdown): add unit tests for `MarkdownUtils` helper functions
2311
+ - fix(mat-date-time): fix timepicker dropdown popup position in desktop mode by attaching directive directly to the visible input
2312
+ - fix(animation) Fix warning in the JS console, on expansionAnimation
2313
+ - fix(settings-page) Local settings was always overridden by account settings - Fix issue sumaris-app#1755
2314
+ - fix(functions) cherrypick fixes on functions, from v18.x
@@ -0,0 +1,502 @@
1
+ import * as path from 'node:path';
2
+ import * as fs from 'node:fs';
3
+ import { test } from '@playwright/test';
4
+
5
+ // Standalone startup constants for the testing package.
6
+ // Must be kept in sync with src/app/core/services/startup.constants.ts
7
+ const STARTUP_DATA_STORAGE_KEY = '__appStartupData';
8
+
9
+ /**
10
+ * Utility selectors and helpers for Ionic components in e2e tests.
11
+ */
12
+ const IonicSelectors = {
13
+ /** Ionic button */
14
+ button: 'ion-button',
15
+ /** Ionic modal */
16
+ modal: 'ion-modal',
17
+ /** Ionic content area */
18
+ content: 'ion-content',
19
+ /** Ionic item */
20
+ item: 'ion-item',
21
+ /** Ionic label */
22
+ label: 'ion-label',
23
+ /** Ionic spinner */
24
+ spinner: 'ion-spinner',
25
+ /** Ionic skeleton text (loading placeholder) */
26
+ skeletonText: 'ion-skeleton-text',
27
+ /** Ionic back button */
28
+ backButton: 'ion-back-button',
29
+ /** Ionic toolbar */
30
+ toolbar: 'ion-toolbar',
31
+ /** Ionic footer */
32
+ footer: 'ion-footer',
33
+ /** Ionic header */
34
+ header: 'ion-header',
35
+ };
36
+ /**
37
+ * Helper functions for interacting with Ionic components.
38
+ */
39
+ class IonicHelper {
40
+ page;
41
+ constructor(page) {
42
+ this.page = page;
43
+ }
44
+ /** Get an Ionic button by text content */
45
+ buttonByText(text) {
46
+ return this.page.locator(IonicSelectors.button, { hasText: text });
47
+ }
48
+ /** Get an Ionic button by CSS selector */
49
+ button(selector, options) {
50
+ const fullSelector = selector ? `${IonicSelectors.button}${selector}` : IonicSelectors.button;
51
+ return this.page.locator(fullSelector, options);
52
+ }
53
+ /**
54
+ * Get an Ionic modal by its inner component selector (e.g. 'app-auth-form').
55
+ * Automatically wraps the selector in `ion-modal:has(...)` so callers
56
+ * only need to provide the component tag name.
57
+ */
58
+ modal(selector) {
59
+ const fullSelector = selector.startsWith('ion-modal') ? selector : `ion-modal:has(${selector})`;
60
+ return this.page.locator(fullSelector);
61
+ }
62
+ /** Wait for the Ionic app to be ready */
63
+ async waitForAppReady(timeout = 15_000) {
64
+ // Wait for ion-app to be attached (Angular bootstrap complete)
65
+ await this.page.waitForSelector('ion-app', { state: 'attached', timeout });
66
+ }
67
+ /** Wait for loading spinners to disappear */
68
+ async waitForLoadingComplete(timeout = 15_000) {
69
+ await this.page
70
+ .locator(IonicSelectors.spinner)
71
+ .waitFor({ state: 'hidden', timeout })
72
+ .catch(() => {
73
+ // Spinner may not appear at all — that's fine
74
+ });
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Utility selectors and helpers for Angular Material components in e2e tests.
80
+ */
81
+ const MaterialSelectors = {
82
+ /** Material table row */
83
+ row: 'mat-row, tr[mat-row]',
84
+ /** Material header row */
85
+ headerRow: 'mat-header-row, tr[mat-header-row]',
86
+ /** Material cell */
87
+ cell: 'mat-cell, td[mat-cell]',
88
+ /** Material form field */
89
+ formField: 'mat-form-field',
90
+ /** Material input */
91
+ input: 'input[matInput]',
92
+ /** Material tab group */
93
+ tabGroup: 'mat-tab-group',
94
+ /** Material tab label */
95
+ tabLabel: '.mat-mdc-tab',
96
+ /** Material paginator */
97
+ paginator: 'mat-paginator',
98
+ /** Material icon */
99
+ icon: 'mat-icon',
100
+ /** Material autocomplete panel */
101
+ autocompletePanel: '.mat-mdc-autocomplete-panel',
102
+ /** Material autocomplete option */
103
+ autocompleteOption: 'mat-option',
104
+ };
105
+ /**
106
+ * Helper functions for interacting with Angular Material components.
107
+ */
108
+ class MaterialHelper {
109
+ page;
110
+ constructor(page) {
111
+ this.page = page;
112
+ }
113
+ /** Get all visible table rows, optionally scoped to a parent locator */
114
+ tableRows(scope) {
115
+ const root = scope ?? this.page;
116
+ return root.locator(MaterialSelectors.row);
117
+ }
118
+ /** Get a single table row by index (0-based), optionally scoped to a parent locator */
119
+ tableRow(index, scope) {
120
+ return this.tableRows(scope).nth(index);
121
+ }
122
+ /** Click on a table row by index (0-based), optionally scoped to a parent locator */
123
+ async clickRow(index, scope) {
124
+ await this.tableRow(index, scope).click();
125
+ }
126
+ /** Get a tab by its label text */
127
+ tabByLabel(text) {
128
+ return this.page.locator(MaterialSelectors.tabLabel, { hasText: text });
129
+ }
130
+ /** Click a tab by its label text */
131
+ async clickTab(text) {
132
+ await this.tabByLabel(text).click();
133
+ }
134
+ /** Fill a mat-form-field input identified by its formControlName.
135
+ * Supports both native inputs with formcontrolname and custom components (e.g. mat-autocomplete-field) wrapping an input. */
136
+ async fillFormControl(formControlName, value) {
137
+ // Try native input first, then fall back to custom component wrapper
138
+ const nativeInput = this.page.locator(`${MaterialSelectors.formField} input[formcontrolname="${formControlName}"]`);
139
+ const wrappedInput = this.page.locator(`[formcontrolname="${formControlName}"] input`);
140
+ const input = (await nativeInput.count()) > 0 ? nativeInput : wrappedInput;
141
+ // Click the parent mat-form-field to ensure focus reaches the input
142
+ const formField = input.locator('xpath=ancestor::mat-form-field');
143
+ if ((await formField.count()) > 0) {
144
+ await formField.first().click();
145
+ }
146
+ else {
147
+ await input.click({ force: true });
148
+ }
149
+ await input.pressSequentially(value, { delay: 50 });
150
+ }
151
+ /**
152
+ * Select an option from a mat-select dropdown by formControlName.
153
+ * Opens the dropdown, waits for options to render, clicks the matching option, then waits for the dropdown to close.
154
+ */
155
+ async selectMatSelectOption(formControlName, optionText) {
156
+ const select = this.page.locator(`mat-select[formcontrolname="${formControlName}"]`);
157
+ await select.click();
158
+ // Use role-based selector: mat-select renders options with role="option" in a listbox
159
+ const option = this.page.getByRole('option', { name: optionText });
160
+ await option.waitFor({ state: 'visible', timeout: 10_000 });
161
+ await option.click();
162
+ // Wait for the option to disappear (dropdown closed)
163
+ await option.waitFor({ state: 'hidden', timeout: 5_000 });
164
+ }
165
+ /** Select an autocomplete option by text after typing in a form control */
166
+ async selectAutocompleteOption(formControlName, searchText, optionText) {
167
+ await this.fillFormControl(formControlName, searchText);
168
+ const panel = this.page.locator(MaterialSelectors.autocompletePanel);
169
+ await panel.waitFor({ state: 'visible', timeout: 10_000 });
170
+ await panel.locator(MaterialSelectors.autocompleteOption, { hasText: optionText }).click();
171
+ }
172
+ /** Wait for table rows to be loaded (at least one row visible), optionally scoped to a parent locator */
173
+ async waitForTableRows(timeout = 15_000, scope) {
174
+ await this.tableRows(scope).first().waitFor({ state: 'visible', timeout });
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Common UI helper functions to improve readability of e2e tests.
180
+ * This helper centralizes access to Ionic and Material specific helpers.
181
+ */
182
+ class UiHelper {
183
+ page;
184
+ ionic;
185
+ mat;
186
+ constructor(page) {
187
+ this.page = page;
188
+ this.ionic = new IonicHelper(page);
189
+ this.mat = new MaterialHelper(page);
190
+ }
191
+ /**
192
+ * Click an element and wait for it to be ready.
193
+ * @param locator The locator to click.
194
+ * @param timeout The maximum time to wait.
195
+ */
196
+ async click(locator, timeout = 10_000) {
197
+ await locator.waitFor({ state: 'visible', timeout });
198
+ // force: true bypasses Ionic overlay elements (ion-col, ion-toolbar) that intercept pointer events
199
+ await locator.click({ force: true });
200
+ }
201
+ /**
202
+ * Fill an input and wait for it to be ready.
203
+ * @param locator The locator to fill.
204
+ * @param value The value to enter.
205
+ * @param timeout The maximum time to wait.
206
+ */
207
+ async fill(locator, value, timeout = 10_000) {
208
+ await locator.waitFor({ state: 'visible', timeout });
209
+ await locator.fill(value);
210
+ }
211
+ /**
212
+ * Wait for an element to be visible.
213
+ * @param locator The locator to wait for.
214
+ * @param timeout The maximum time to wait.
215
+ */
216
+ async waitForVisible(locator, timeout = 10_000) {
217
+ await locator.waitFor({ state: 'visible', timeout });
218
+ }
219
+ /**
220
+ * Wait for an element to be hidden.
221
+ * @param locator The locator to wait for.
222
+ * @param timeout The maximum time to wait.
223
+ */
224
+ async waitForHidden(locator, timeout = 15_000) {
225
+ await locator.waitFor({ state: 'hidden', timeout });
226
+ }
227
+ /* --- Unified Selectors --- */
228
+ /**
229
+ * Get a button by its text content.
230
+ * Supports both Ionic and Material buttons.
231
+ * @param text The text content to match.
232
+ * @param scope Optional locator to scope the search within.
233
+ */
234
+ button(text, scope) {
235
+ // Combine selectors for both Ionic and Material buttons
236
+ const selectors = [
237
+ IonicSelectors.button,
238
+ 'button[mat-button]',
239
+ 'button[mat-raised-button]',
240
+ 'button[mat-flat-button]',
241
+ 'button[mat-stroked-button]',
242
+ 'button[mat-icon-button]',
243
+ 'button[mat-fab]',
244
+ 'button[mat-mini-fab]',
245
+ ].join(', ');
246
+ const root = scope ?? this.page;
247
+ return root.locator(selectors, { hasText: text });
248
+ }
249
+ /**
250
+ * Get an icon button by its icon name.
251
+ * Supports both Ionic and Material icon buttons.
252
+ * @param iconName The icon name (e.g. 'mail', 'search', 'ellipsis-horizontal').
253
+ * @param scope Optional locator to scope the search within.
254
+ */
255
+ iconButton(iconName, scope) {
256
+ const matIconSelector = `${MaterialSelectors.icon}:has-text("${iconName}")`;
257
+ const ionIconSelector = `ion-icon[name="${iconName}"]`;
258
+ const root = scope ?? this.page;
259
+ return root.locator(`button, ion-button`).filter({
260
+ has: this.page.locator(`${matIconSelector}, ${ionIconSelector}`),
261
+ });
262
+ }
263
+ /**
264
+ * Access to the underlying Playwright Page object.
265
+ */
266
+ get pageObj() {
267
+ return this.page;
268
+ }
269
+ /** Get a modal by its selector */
270
+ modal(selector) {
271
+ return this.ionic.modal(selector);
272
+ }
273
+ /** Get a tab by its label text */
274
+ tab(text) {
275
+ return this.mat.tabByLabel(text);
276
+ }
277
+ /* --- Specialized Helpers (delegated) --- */
278
+ /** Wait for the app to be ready */
279
+ async waitForAppReady(timeout = 15_000) {
280
+ await this.ionic.waitForAppReady(timeout);
281
+ }
282
+ /** Wait for loading spinners to disappear */
283
+ async waitForLoadingComplete(timeout = 15_000) {
284
+ await this.ionic.waitForLoadingComplete(timeout);
285
+ }
286
+ /** Wait for table rows to be loaded, optionally scoped to a parent locator */
287
+ async waitForTableRows(timeout = 15_000, scope) {
288
+ await this.mat.waitForTableRows(timeout, scope);
289
+ }
290
+ /** Get a single table row by index (0-based), optionally scoped to a parent locator */
291
+ tableRow(index, scope) {
292
+ return this.mat.tableRow(index, scope);
293
+ }
294
+ /** Click on a table row by index, optionally scoped to a parent locator */
295
+ async clickRow(index, scope) {
296
+ await this.click(this.tableRow(index, scope));
297
+ }
298
+ /** Fill a mat-form-field input identified by its formControlName */
299
+ async fillFormControl(formControlName, value) {
300
+ await this.mat.fillFormControl(formControlName, value);
301
+ }
302
+ /** Select an autocomplete option */
303
+ async selectAutocompleteOption(formControlName, searchText, optionText) {
304
+ await this.mat.selectAutocompleteOption(formControlName, searchText, optionText);
305
+ }
306
+ /**
307
+ * Select an option from a mat-select dropdown.
308
+ * Opens the dropdown, waits for options, clicks the matching one, then waits for the dropdown to close.
309
+ */
310
+ async selectOption(formControlName, optionText) {
311
+ await this.mat.selectMatSelectOption(formControlName, optionText);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Helper for authentication in e2e tests.
317
+ */
318
+ class AuthHelper {
319
+ page;
320
+ ui;
321
+ constructor(page, ui) {
322
+ this.page = page;
323
+ this.ui = ui ?? new UiHelper(page);
324
+ }
325
+ /** Login with given test credentials */
326
+ async login(username, password) {
327
+ const ui = this.ui;
328
+ await this.page.goto('/');
329
+ // Wait for the app to fully load (backend connection can take time)
330
+ await ui.waitForAppReady(30_000);
331
+ await ui.waitForLoadingComplete(30_000);
332
+ // Click the login button on the home page
333
+ const loginButton = ui.button(/Se connecter|Log In/).first();
334
+ await ui.click(loginButton, 30_000);
335
+ // Wait for the auth modal to appear
336
+ const modal = ui.modal('app-auth-form');
337
+ await ui.waitForVisible(modal);
338
+ // Fill credentials (may or may not be pre-filled depending on the environment)
339
+ const usernameInput = modal.locator('input[formcontrolname="username"]');
340
+ await usernameInput.click({ force: true });
341
+ await usernameInput.fill(username);
342
+ const passwordInput = modal.locator('input[formcontrolname="password"]');
343
+ await passwordInput.click({ force: true });
344
+ await passwordInput.fill(password);
345
+ // Click the submit button in the modal footer
346
+ const ionModal = ui.pageObj.locator('ion-modal:has(app-auth-form)');
347
+ const submitButton = ionModal.locator('ion-footer ion-button[color="tertiary"]');
348
+ await submitButton.waitFor({ state: 'visible', timeout: 10_000 });
349
+ // Wait for the button to be enabled (form valid)
350
+ await ui.pageObj.waitForFunction((btn) => !btn.hasAttribute('disabled'), await submitButton.elementHandle(), { timeout: 5_000 });
351
+ await submitButton.click();
352
+ // Wait for the modal to close and verify the user is logged in
353
+ await ui.waitForHidden(modal, 30_000);
354
+ // After login, the login button should no longer be visible (replaced by user menu)
355
+ await loginButton.waitFor({ state: 'hidden', timeout: 10_000 });
356
+ }
357
+ /** Wait until the user is logged in and return the email shown in the side menu */
358
+ async waitForLoggedIn() {
359
+ const emailEl = this.page.locator('ion-menu h4').first();
360
+ await emailEl.waitFor({ state: 'visible', timeout: 30_000 });
361
+ return emailEl.textContent();
362
+ }
363
+ /** Setup startup data with provided credentials or environment variables */
364
+ async loginByEnv(opts) {
365
+ const username = opts?.username ?? process.env.E2E_USERNAME;
366
+ const password = opts?.password ?? process.env.E2E_PASSWORD;
367
+ const offline = opts?.offline ?? process.env.E2E_OFFLINE === 'true';
368
+ const peerUrl = opts?.peerUrl ?? process.env.E2E_PEER_URL;
369
+ if (!username && !password && !peerUrl)
370
+ return;
371
+ await this.page.addInitScript(({ key, username, password, offline, peerUrl }) => {
372
+ window[key] = { username, password, offline, peerUrl };
373
+ }, { key: STARTUP_DATA_STORAGE_KEY, username, password, offline, peerUrl });
374
+ }
375
+ }
376
+
377
+ // GPL v3 License
378
+ /**
379
+ * Search environments based on provided criteria.
380
+ *
381
+ * Use when you need to run tests with one or more environments.
382
+ * Test can use `testWithEnv` to run tests with a specific environment.
383
+ * Example :
384
+ *
385
+ * ```
386
+ * test.describe('Sample test with environment', () => {
387
+ * withEnvs({
388
+ * peerTags: ['local'],
389
+ * userTags: ['admin'],
390
+ * }).forEach((env) => {
391
+ * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {
392
+ * // Test implementation
393
+ * });
394
+ * });
395
+ * });
396
+ * ```
397
+ *
398
+ * Environments are loaded from a JSON config file located at one of four paths :
399
+ * - `LOGIN_ENVS_PATH` environment variable
400
+ * - `.environments.json` in the current directory (e.g. `.environments.json`)
401
+ * - `.environments.json` in the `.local` directory (e.g. `.local/environments.json`)
402
+ * - `.environments.json` in the `e2e` directory (e.g. `e2e/.environments.json`)
403
+ *
404
+ * Example of JSON :
405
+ *
406
+ * ```
407
+ * {
408
+ * "peers": [
409
+ * {
410
+ * "id": "local",
411
+ * "tags": ["local"],
412
+ * "url": "http://localhost:8080",
413
+ * "users": [
414
+ * {
415
+ * "tags": ["admin"],
416
+ * "username": "admin@sumaris.net",
417
+ * "password": "admin"
418
+ * }
419
+ * ]
420
+ * }
421
+ * ]
422
+ * }
423
+ * ```
424
+ *
425
+ * @see testWithEnv
426
+ * @param filter Search criteria.
427
+ */
428
+ function withEnvs(filter) {
429
+ const startupEnvs = [];
430
+ // Search startup env config file
431
+ const candidates = [
432
+ process.env.LOGIN_ENVS_PATH,
433
+ path.resolve(process.cwd(), '.environments.json'),
434
+ path.resolve(process.cwd(), '.local/environments.json'),
435
+ path.resolve(process.cwd(), 'e2e/.environments.json'),
436
+ ].filter(Boolean);
437
+ const environmentsFile = candidates.find((p) => fs.existsSync(p));
438
+ if (!environmentsFile) {
439
+ throw new Error(`Startup envs config file not found.\nTried: ${candidates.join('\n')}`);
440
+ }
441
+ // Load startup config file
442
+ let config;
443
+ try {
444
+ config = JSON.parse(fs.readFileSync(environmentsFile, 'utf8'));
445
+ }
446
+ catch (error) {
447
+ console.error('Error parsing startup envs config file:', error);
448
+ throw error;
449
+ }
450
+ finally {
451
+ fs.closeSync(fs.openSync(environmentsFile, 'r'));
452
+ }
453
+ // Filter startup envs based on criteria
454
+ for (const peerConfig of config.peers) {
455
+ const matchPeerId = filter.peerId && peerConfig.id === filter.peerId;
456
+ const matchPeerTag = filter.peerTags && peerConfig.tags.some((tag) => filter.peerTags?.includes(tag));
457
+ if (!matchPeerId && !matchPeerTag)
458
+ continue;
459
+ for (const userConfig of peerConfig.users) {
460
+ const matchUserTag = filter.userTags && userConfig.tags.some((tag) => filter.userTags?.includes(tag));
461
+ if (!matchUserTag)
462
+ continue;
463
+ startupEnvs.push({
464
+ id: peerConfig.id,
465
+ peerUrl: peerConfig.url,
466
+ username: userConfig.username,
467
+ password: userConfig.password,
468
+ });
469
+ }
470
+ }
471
+ return startupEnvs;
472
+ }
473
+ /**
474
+ * Currying function that returns a test function that can be used to run tests with a specific environment.
475
+ *
476
+ * ```
477
+ * testWithEnv(env)(`Some test with environment ${env.id}`, async ({ page }) => {
478
+ * // Test implementation
479
+ * })
480
+ * ```
481
+ *
482
+ * @see withEnvs
483
+ * @param env Environment to use for the test.
484
+ */
485
+ function testWithEnv(env) {
486
+ return (title, testFn) => {
487
+ test(title, async ({ page, context: playwrightContext, browser, request }) => {
488
+ const auth = new AuthHelper(page);
489
+ await auth.loginByEnv(env);
490
+ await testFn({ page, context: playwrightContext, browser, request });
491
+ });
492
+ };
493
+ }
494
+
495
+ // constants
496
+
497
+ /**
498
+ * Generated bundle index. Do not edit.
499
+ */
500
+
501
+ export { AuthHelper, IonicHelper, IonicSelectors, MaterialHelper, MaterialSelectors, STARTUP_DATA_STORAGE_KEY, UiHelper, testWithEnv, withEnvs };
502
+ //# sourceMappingURL=sumaris-net-ngx-components-testing.mjs.map