@networkpro/web 1.25.9 → 1.25.11

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/.env.template CHANGED
@@ -5,7 +5,7 @@
5
5
  # Rename to `.env` (or `.env.local`) and customize as needed
6
6
 
7
7
  # Custom environment mode for scripts and tooling
8
- # One of: dev, test, ci, preview, production
8
+ # One of: dev, test, ci, audit, production
9
9
  ENV_MODE=dev
10
10
 
11
11
  # Optional: API keys or tokens for local dev (never commit real values)
@@ -25,6 +25,8 @@ on:
25
25
  jobs:
26
26
  auto-assign:
27
27
  runs-on: ubuntu-24.04
28
+ env:
29
+ ENV_MODE: ci
28
30
 
29
31
  steps:
30
32
  - name: 'Auto-assign issue or PR'
@@ -22,6 +22,8 @@ jobs:
22
22
  name: Sync backup/nightly-snapshot to master
23
23
  runs-on: ubuntu-24.04
24
24
  if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request'
25
+ env:
26
+ ENV_MODE: ci
25
27
 
26
28
  steps:
27
29
  - name: Checkout master branch
@@ -42,11 +42,6 @@ jobs:
42
42
  cache: npm
43
43
  cache-dependency-path: package-lock.json
44
44
 
45
- #- name: Show Node.js and npm versions
46
- # run: |
47
- # echo "Node.js version: $(node -v)"
48
- # echo "npm version: $(npm -v)"
49
-
50
45
  - name: Upgrade npm
51
46
  run: |
52
47
  corepack enable
@@ -132,11 +127,6 @@ jobs:
132
127
  cache: npm
133
128
  cache-dependency-path: package-lock.json
134
129
 
135
- #- name: Show Node.js and npm versions
136
- # run: |
137
- # echo "Node.js version: $(node -v)"
138
- # echo "npm version: $(npm -v)"
139
-
140
130
  - name: Upgrade npm
141
131
  run: |
142
132
  corepack enable
@@ -193,11 +183,6 @@ jobs:
193
183
  cache: npm
194
184
  cache-dependency-path: package-lock.json
195
185
 
196
- #- name: Show Node.js and npm versions
197
- # run: |
198
- # echo "Node.js version: $(node -v)"
199
- # echo "npm version: $(npm -v)"
200
-
201
186
  - name: Upgrade npm
202
187
  run: |
203
188
  corepack enable
@@ -12,6 +12,8 @@ jobs:
12
12
  check-expiry:
13
13
  runs-on: ubuntu-latest
14
14
  name: Validate .well-known/security.txt expiration
15
+ env:
16
+ ENV_MODE: ci
15
17
 
16
18
  steps:
17
19
  - name: Checkout repo
@@ -13,6 +13,8 @@ permissions:
13
13
  jobs:
14
14
  dependency-review:
15
15
  runs-on: ubuntu-24.04
16
+ env:
17
+ ENV_MODE: ci
16
18
 
17
19
  steps:
18
20
  - name: 'Checkout Repository'
@@ -18,6 +18,9 @@ permissions:
18
18
  jobs:
19
19
  meta:
20
20
  runs-on: ubuntu-24.04
21
+ env:
22
+ ENV_MODE: ci
23
+
21
24
  steps:
22
25
  - name: Checkout repo
23
26
  uses: actions/checkout@v5
@@ -16,6 +16,9 @@ permissions:
16
16
  jobs:
17
17
  check-branch:
18
18
  runs-on: ubuntu-24.04
19
+ env:
20
+ ENV_MODE: ci
21
+
19
22
  steps:
20
23
  - name: Fail if source is audit-netlify
21
24
  run: |
@@ -21,6 +21,8 @@ jobs:
21
21
  issues: write
22
22
  env:
23
23
  CODEQL_ACTION_ANALYSIS_KEY: gitleaks
24
+ ENV_MODE: ci
25
+
24
26
  steps:
25
27
  # ---------------------------------------------------------------------
26
28
  # Checkout the full repo history (needed for Gitleaks to scan all commits)
@@ -24,6 +24,8 @@ jobs:
24
24
  check:
25
25
  name: Check CodeQL Status
26
26
  runs-on: ubuntu-24.04
27
+ env:
28
+ ENV_MODE: ci
27
29
 
28
30
  steps:
29
31
  - name: Check CodeQL Workflow
@@ -50,11 +50,6 @@ jobs:
50
50
  cache: npm
51
51
  cache-dependency-path: package-lock.json
52
52
 
53
- #- name: Show Node.js and npm versions
54
- # run: |
55
- # echo "Node.js version: $(node -v)"
56
- # echo "npm version: $(npm -v)"
57
-
58
53
  - name: Upgrade npm
59
54
  run: |
60
55
  corepack enable
@@ -136,11 +131,6 @@ jobs:
136
131
  cache: npm
137
132
  cache-dependency-path: package-lock.json
138
133
 
139
- #- name: Show Node.js and npm versions
140
- # run: |
141
- # echo "Node.js version: $(node -v)"
142
- # echo "npm version: $(npm -v)"
143
-
144
134
  - name: Upgrade npm
145
135
  run: |
146
136
  corepack enable
@@ -197,11 +187,6 @@ jobs:
197
187
  cache: npm
198
188
  cache-dependency-path: package-lock.json
199
189
 
200
- #- name: Show Node.js and npm versions
201
- # run: |
202
- # echo "Node.js version: $(node -v)"
203
- # echo "npm version: $(npm -v)"
204
-
205
190
  - name: Upgrade npm
206
191
  run: |
207
192
  corepack enable
package/CHANGELOG.md CHANGED
@@ -22,6 +22,55 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
22
22
 
23
23
  ---
24
24
 
25
+ ## [1.25.11] - 2025-11-12
26
+
27
+ ### Added
28
+
29
+ - `gotoDesktop(page, path)` and `gotoMobile(page, path)` helper functions to streamline viewport + navigation setup.
30
+ - `clickAndWaitForNavigation(page, locator, options)` utility for safe SPA or full-page navigation detection with optional URL pattern matching.
31
+ - `DEBUG_LOGS` flag in `helpers.js` to allow toggling of console logs for test diagnostics.
32
+ - Navigation debug logs to `getVisibleNav()` to indicate which navigation region was detected (when debugging is enabled).
33
+
34
+ ### Changed
35
+
36
+ - Refactored all E2E tests to use `gotoDesktop()` and `gotoMobile()` for consistency and DRY principles.
37
+ - Replaced brittle direct `waitForNavigation()` usages with `clickAndWaitForNavigation()` helper.
38
+ - Updated mobile and desktop tests to improve consistency across specs and improve visibility assertions.
39
+
40
+ ### Removed
41
+
42
+ - Legacy direct `setViewportSize()` and `page.goto()` calls from individual test blocks (now handled via `goto*()` helpers).
43
+
44
+ ---
45
+
46
+ ## [1.25.10] - 2025-11-12
47
+
48
+ ### Changed
49
+
50
+ - Updated GitHub workflows to specify `ENV: ci` where appropriate:
51
+ - `templates/check-codeql.template.yml`
52
+ - `templates/publish.template.yml`
53
+ - `auto-assign.yml`
54
+ - `branch-backup.yml`
55
+ - `check-security-txt-expiry.yml`
56
+ - `dependency-review.yml`
57
+ - `meta-check.yml`
58
+ - `prevent-audit-merges.yml`
59
+ - `secret-scan.yml`
60
+ - Added `@sveltejs/adapter-netlify` devDependency for smoother toggling between production and audit modes.
61
+ - Production uses `@sveltejs/adapter-vercel` only. `@sveltejs/adapter-netlify` exists solely to support the audit environment.
62
+ - Bumped project version to `v1.25.10`.
63
+ - Updated dependencies:
64
+ - `@testing-library/svelte` `^5.2.8` → `^5.2.9`
65
+ - `eslint-plugin-jsdoc` `^61.1.12` → `^61.2.0`
66
+ - `posthog-js` `^1.290.0` → `^1.292.0`
67
+
68
+ ## Removed
69
+
70
+ - Removed unneeded comments in `build-and-publish.yml` workflow.
71
+
72
+ ---
73
+
25
74
  ## [1.25.9] - 2025-11-11
26
75
 
27
76
  ### Changed
@@ -1827,7 +1876,9 @@ This enables analytics filtering and CSP hardening for the audit environment.
1827
1876
 
1828
1877
  <!-- Link references -->
1829
1878
 
1830
- [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.25.9...HEAD
1879
+ [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.25.11...HEAD
1880
+ [1.25.11]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.25.11
1881
+ [1.25.10]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.25.10
1831
1882
  [1.25.9]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.25.9
1832
1883
  [1.25.8]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.25.8
1833
1884
  [1.25.7]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.25.7
package/cspell.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "CCPA",
15
15
  "CPRA",
16
16
  "cryptomator",
17
+ "domcontentloaded",
17
18
  "dont",
18
19
  "elif",
19
20
  "Embedder",
@@ -28,6 +29,7 @@
28
29
  "homescreen",
29
30
  "HREFTOP",
30
31
  "HSTS",
32
+ "interactable",
31
33
  "Izzy",
32
34
  "Keybase",
33
35
  "keypair",
@@ -40,6 +42,7 @@
40
42
  "lightningcss",
41
43
  "linksheet",
42
44
  "linktree",
45
+ "LOCALAPPDATA",
43
46
  "lsheet",
44
47
  "Mailvelope",
45
48
  "Maricopa",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@networkpro/web",
3
3
  "private": false,
4
- "version": "1.25.9",
4
+ "version": "1.25.11",
5
5
  "description": "Locking Down Networks, Unlocking Confidence™ | Security, Networking, Privacy — Network Pro Strategies",
6
6
  "keywords": [
7
7
  "advisory",
@@ -54,6 +54,7 @@
54
54
  "verify": "npm run checkout",
55
55
  "delete": "rm -rf .svelte-kit node_modules package-lock.json",
56
56
  "clean": "npm run delete && npm cache clean --force && npm install",
57
+ "repair": "npm run delete && npx playwright uninstall && rm -rf %LOCALAPPDATA%/ms-playwright && npm install && npx playwright install",
57
58
  "upgrade": "ncu -u --format group --color",
58
59
  "check:updates": "ncu --format group --color",
59
60
  "test": "npm run test:all",
@@ -85,7 +86,7 @@
85
86
  },
86
87
  "dependencies": {
87
88
  "dompurify": "^3.3.0",
88
- "posthog-js": "^1.290.0",
89
+ "posthog-js": "^1.292.0",
89
90
  "semver": "^7.7.3",
90
91
  "svelte": "5.43.6"
91
92
  },
@@ -94,17 +95,18 @@
94
95
  "@eslint/js": "^9.39.1",
95
96
  "@lhci/cli": "^0.15.1",
96
97
  "@playwright/test": "^1.56.1",
98
+ "@sveltejs/adapter-netlify": "^5.2.4",
97
99
  "@sveltejs/adapter-vercel": "^6.1.1",
98
100
  "@sveltejs/kit": "2.48.4",
99
101
  "@sveltejs/vite-plugin-svelte": "^6.2.1",
100
102
  "@testing-library/jest-dom": "^6.9.1",
101
- "@testing-library/svelte": "^5.2.8",
103
+ "@testing-library/svelte": "^5.2.9",
102
104
  "@vitest/coverage-v8": "3.2.4",
103
105
  "autoprefixer": "^10.4.22",
104
106
  "browserslist": "^4.28.0",
105
107
  "eslint": "^9.39.1",
106
108
  "eslint-config-prettier": "^10.1.8",
107
- "eslint-plugin-jsdoc": "^61.1.12",
109
+ "eslint-plugin-jsdoc": "^61.2.0",
108
110
  "eslint-plugin-svelte": "^3.13.0",
109
111
  "globals": "^16.5.0",
110
112
  "jsdom": "26.1.0",
@@ -15,7 +15,7 @@ This file is part of Network Pro.
15
15
  * @updated 2025-11-11
16
16
  */
17
17
 
18
- /** @typedef {{ ua: string, ip: string }} ScannerInput */
18
+ /** @typedef {{ ua?: string, ip?: string }} ScannerInput */
19
19
 
20
20
  /**
21
21
  * Check if a request is likely from Probely.
@@ -23,7 +23,13 @@ This file is part of Network Pro.
23
23
  * @returns {boolean} - True if the request matches Probely’s fingerprint.
24
24
  */
25
25
  export function isProbelyScanner({ ua, ip }) {
26
- const PROBELY_UA_FRAGMENT = 'probelyspdr/';
26
+ const PROBELY_UA_FRAGMENTS = [
27
+ 'probelyspdr/',
28
+ 'probelyfp/',
29
+ 'probelymrkt/',
30
+ 'probelysc/',
31
+ 'python-httpx/',
32
+ ];
27
33
  const PROBELY_IPS = [
28
34
  '18.235.241.170',
29
35
  '52.65.214.19',
@@ -41,7 +47,10 @@ export function isProbelyScanner({ ua, ip }) {
41
47
  const normalizedIP = ip?.trim() ?? '';
42
48
 
43
49
  return (
44
- normalizedUA.includes(PROBELY_UA_FRAGMENT) ||
45
- PROBELY_IPS.includes(normalizedIP)
50
+ PROBELY_UA_FRAGMENTS.some((fragment) =>
51
+ normalizedUA.includes(fragment.toLowerCase()),
52
+ ) || PROBELY_IPS.includes(normalizedIP)
46
53
  );
47
54
  }
55
+
56
+ // cspell:ignore probelyfp probelymrkt probelysc httpx
@@ -11,15 +11,16 @@ This file is part of Network Pro.
11
11
  * @description Runs Playwright E2E tests with desktop and root route assertions.
12
12
  * @module tests/e2e
13
13
  * @author Scott Lopez
14
- * @updated 2025-10-21
14
+ * @updated 2025-11-12
15
15
  */
16
16
 
17
17
  import { expect, test } from '@playwright/test';
18
18
  import {
19
+ clickAndWaitForNavigation,
19
20
  getFooter,
20
21
  getVisibleNav,
21
- setDesktopView,
22
- setMobileView,
22
+ gotoDesktop,
23
+ gotoMobile,
23
24
  } from './shared/helpers.js';
24
25
 
25
26
  // Root route should display nav bar and about link
@@ -29,9 +30,7 @@ test.describe('Desktop Tests', () => {
29
30
  test("should display the navigation bar and 'about' link", async ({
30
31
  page,
31
32
  }) => {
32
- await setDesktopView(page);
33
- await page.goto('/');
34
- await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
33
+ await gotoDesktop(page);
35
34
 
36
35
  const nav = await getVisibleNav(page);
37
36
 
@@ -42,9 +41,7 @@ test.describe('Desktop Tests', () => {
42
41
 
43
42
  // Root route should display the footer properly
44
43
  test('should display the footer correctly', async ({ page }) => {
45
- await setDesktopView(page);
46
- await page.goto('/');
47
- await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
44
+ await gotoDesktop(page);
48
45
 
49
46
  const footer = getFooter(page);
50
47
  await expect(footer).toBeVisible();
@@ -54,9 +51,7 @@ test.describe('Desktop Tests', () => {
54
51
  test("should render the 'about' page with footer visible", async ({
55
52
  page,
56
53
  }) => {
57
- await setDesktopView(page);
58
- await page.goto('/about');
59
- await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
54
+ await gotoDesktop(page, '/about');
60
55
 
61
56
  const footer = getFooter(page);
62
57
  await expect(footer).toBeVisible();
@@ -64,20 +59,16 @@ test.describe('Desktop Tests', () => {
64
59
 
65
60
  // Root route should have a clickable "about" link
66
61
  test("should ensure the 'about' link is clickable", async ({ page }) => {
67
- await setDesktopView(page);
68
- await page.goto('/');
69
- await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
62
+ await gotoDesktop(page);
70
63
 
71
64
  const nav = await getVisibleNav(page);
72
65
  const aboutLink = nav.getByRole('link', { name: 'about' });
73
- await expect(aboutLink).toBeVisible();
74
- await aboutLink.click();
75
66
 
76
- // safer wait pattern with load state
77
- await Promise.all([
78
- page.waitForLoadState('load'),
79
- page.waitForURL('**/about', { timeout: 60000 }),
80
- ]);
67
+ await clickAndWaitForNavigation(page, aboutLink, {
68
+ urlPattern: /\/about/,
69
+ timeout: 60000,
70
+ });
71
+
81
72
  await expect(page).toHaveURL(/\/about/);
82
73
  });
83
74
  }); // End Desktop Tests
@@ -85,13 +76,10 @@ test.describe('Desktop Tests', () => {
85
76
  // Root route should display headings properly on mobile
86
77
  test.describe('Mobile Tests', () => {
87
78
  test('should display main content correctly on mobile', async ({ page }) => {
88
- await setMobileView(page);
89
- await page.goto('/');
90
- await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
79
+ await gotoMobile(page);
91
80
 
92
- const mainHeading = page.locator('h1, h2');
93
- await expect(mainHeading).toBeVisible();
81
+ // Ensure main headline is present on mobile
82
+ const h2 = page.locator('h2.index-title2');
83
+ await expect(h2).toContainText(/security/i);
94
84
  });
95
85
  });
96
-
97
- // cspell:ignore domcontentloaded
@@ -11,7 +11,7 @@ This file is part of Network Pro.
11
11
  * @description Runs Playwright E2E tests with mobile assertions.
12
12
  * @module tests/e2e
13
13
  * @author Scott Lopez
14
- * @updated 2025-10-21
14
+ * @updated 2025-11-12
15
15
  */
16
16
 
17
17
  import { expect, test } from '@playwright/test';
@@ -19,7 +19,7 @@ import {
19
19
  clickAndWaitForNavigation,
20
20
  getFooter,
21
21
  getVisibleNav,
22
- setMobileView,
22
+ gotoMobile,
23
23
  } from './shared/helpers.js';
24
24
 
25
25
  // ---------------------------------------------------------------------------
@@ -27,21 +27,20 @@ import {
27
27
  // ---------------------------------------------------------------------------
28
28
 
29
29
  test.describe('Mobile Tests', () => {
30
- // Increase timeout for all mobile tests
31
30
  test.setTimeout(90_000);
32
31
 
32
+ test.beforeEach(({ browserName }) => {
33
+ if (browserName === 'webkit')
34
+ test.skip('Skipping WebKit: manual validation only');
35
+ });
36
+
33
37
  // -------------------------------------------------------------------------
34
38
  // 🧱 Test 1: Main description text
35
39
  // -------------------------------------------------------------------------
36
40
  test('should display the main description text on mobile', async ({
37
41
  page,
38
- browserName,
39
42
  }) => {
40
- if (browserName === 'webkit') test.skip();
41
-
42
- await setMobileView(page);
43
- await page.goto('/');
44
- await page.waitForLoadState('domcontentloaded', { timeout: 60_000 });
43
+ await gotoMobile(page);
45
44
 
46
45
  const description = page.locator(
47
46
  'div.index-title1:has-text("Locking Down Networks")',
@@ -52,18 +51,11 @@ test.describe('Mobile Tests', () => {
52
51
  // -------------------------------------------------------------------------
53
52
  // 🧱 Test 2: Main content
54
53
  // -------------------------------------------------------------------------
55
- test('should display main content correctly on mobile', async ({
56
- page,
57
- browserName,
58
- }) => {
59
- if (browserName === 'webkit') test.skip();
60
-
61
- await setMobileView(page);
62
- await page.goto('/');
63
- await page.waitForLoadState('domcontentloaded', { timeout: 60_000 });
54
+ test('should display main content correctly on mobile', async ({ page }) => {
55
+ await gotoMobile(page);
64
56
 
65
- const mainHeading = page.locator('h1, h2');
66
- await expect(mainHeading).toBeVisible();
57
+ const mainHeading = page.locator('h2.index-title2');
58
+ await expect(mainHeading).toContainText(/security/i);
67
59
  });
68
60
 
69
61
  // -------------------------------------------------------------------------
@@ -71,41 +63,27 @@ test.describe('Mobile Tests', () => {
71
63
  // -------------------------------------------------------------------------
72
64
  test("should ensure the 'about' link is clickable on mobile", async ({
73
65
  page,
74
- browserName,
75
66
  }) => {
76
- if (browserName === 'webkit') test.skip();
77
-
78
- await setMobileView(page);
79
- await page.goto('/', { waitUntil: 'networkidle' });
67
+ await gotoMobile(page);
80
68
 
81
69
  const nav = await getVisibleNav(page);
82
-
83
- // Ensure the link exists before interacting
84
- await page.waitForSelector('a[href="/about"]', { timeout: 10_000 });
85
70
  const aboutLink = nav.getByRole('link', { name: 'about' });
86
71
 
87
- // Use the safe navigation helper
88
72
  await clickAndWaitForNavigation(page, aboutLink, {
89
73
  urlPattern: /\/about/,
90
74
  timeout: 60_000,
91
75
  });
76
+
77
+ await expect(page).toHaveURL(/\/about/);
92
78
  });
93
79
 
94
80
  // -------------------------------------------------------------------------
95
81
  // 🧱 Test 4: Footer presence on /about
96
82
  // -------------------------------------------------------------------------
97
- test('should display the footer on /about (mobile)', async ({
98
- page,
99
- browserName,
100
- }) => {
101
- if (browserName === 'webkit') test.skip();
102
-
103
- await setMobileView(page);
104
- await page.goto('/about');
83
+ test('should display the footer on /about (mobile)', async ({ page }) => {
84
+ await gotoMobile(page, '/about');
105
85
 
106
86
  const footer = getFooter(page);
107
87
  await expect(footer).toBeVisible();
108
88
  });
109
89
  });
110
-
111
- // cspell:ignore domcontentloaded networkidle
@@ -11,19 +11,19 @@ This file is part of Network Pro.
11
11
  * @description Stores commonly used functions for importing into E2E tests.
12
12
  * @module tests/e2e/shared
13
13
  * @author Scott Lopez
14
- * @updated 2025-10-29
14
+ * @updated 2025-11-12
15
15
  */
16
16
 
17
- import { expect } from '@playwright/test';
17
+ const DEBUG_LOGS = false; // set to true to enable console logs
18
18
 
19
19
  /**
20
- * @param {import('@playwright/test').Page} page - The Playwright page object.
20
+ * @param {import('@playwright/test').Page} page
21
21
  * @returns {Promise<void>}
22
- * @description Sets standard desktop viewport and waits for animations.
22
+ * @description Sets desktop viewport and allows layout to settle.
23
23
  */
24
24
  export async function setDesktopView(page) {
25
25
  await page.setViewportSize({ width: 1280, height: 720 });
26
- await page.waitForTimeout(1500);
26
+ await page.waitForTimeout(1500); // Known-stable stabilization delay
27
27
  }
28
28
 
29
29
  /**
@@ -36,18 +36,47 @@ export async function setMobileView(page) {
36
36
  }
37
37
 
38
38
  /**
39
- * @param {import('@playwright/test').Page} page - The Playwright page object.
40
- * @returns {Promise<import('@playwright/test').Locator>} - A visible navigation locator.
41
- * @throws {Error} If no visible navigation is found.
39
+ * Navigate with desktop viewport + initial DOM load.
40
+ * @param {import('@playwright/test').Page} page
41
+ * @param {string} path
42
+ */
43
+ export async function gotoDesktop(page, path = '/') {
44
+ await setDesktopView(page);
45
+ await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 60000 });
46
+ }
47
+
48
+ /**
49
+ * Navigate with mobile viewport + initial DOM load.
50
+ * @param {import('@playwright/test').Page} page
51
+ * @param {string} path
52
+ */
53
+ export async function gotoMobile(page, path = '/') {
54
+ await setMobileView(page);
55
+ await page.goto(path, { waitUntil: 'domcontentloaded', timeout: 60000 });
56
+ }
57
+
58
+ /**
59
+ * Returns the visible navigation region.
60
+ * @param {import('@playwright/test').Page} page
61
+ * @returns {Promise<import('@playwright/test').Locator>}
42
62
  */
43
63
  export async function getVisibleNav(page) {
44
64
  const navHome = page.getByRole('navigation', { name: 'Homepage navigation' });
45
65
  const navMain = page.getByRole('navigation', { name: 'Main navigation' });
46
66
 
47
- if (await navHome.isVisible()) return navHome;
48
- if (await navMain.isVisible()) return navMain;
67
+ const timeout = 5000;
68
+
69
+ if (await navHome.isVisible({ timeout }).catch(() => false)) {
70
+ if (DEBUG_LOGS) console.log('✅ Detected visible nav: Homepage navigation');
71
+ return navHome;
72
+ }
49
73
 
50
- throw new Error('No visible navigation element found.');
74
+ if (await navMain.isVisible({ timeout }).catch(() => false)) {
75
+ if (DEBUG_LOGS) console.log('✅ Detected visible nav: Main navigation');
76
+ return navMain;
77
+ }
78
+
79
+ throw new Error('❌ No visible navigation element found within timeout.');
51
80
  }
52
81
 
53
82
  /**
@@ -59,37 +88,28 @@ export function getFooter(page) {
59
88
  }
60
89
 
61
90
  /**
62
- * @function clickAndWaitForNavigation
63
- * @description Clicks a link or button and waits for navigation or URL change.
64
- * Works for both SPA (client-side) and full-page navigations.
91
+ * Click + wait for SPA or full navigation event.
65
92
  *
66
93
  * @param {import('@playwright/test').Page} page
67
94
  * @param {import('@playwright/test').Locator} locator
68
- * @param {object} [options]
69
- * @param {string|RegExp} [options.urlPattern] - URL or regex to match
70
- * @param {number} [options.timeout=60000] - Max wait time
95
+ * @param {{ urlPattern?: string | RegExp, timeout?: number }} [options]
71
96
  */
72
97
  export async function clickAndWaitForNavigation(page, locator, options = {}) {
73
98
  const { urlPattern = /\/.*/, timeout = 60000 } = options;
74
99
 
75
- // Ensure the element is ready for interaction
76
100
  await locator.scrollIntoViewIfNeeded();
77
- await locator.waitFor({ state: 'visible', timeout: 10000 });
78
- await locator.click({ trial: true });
101
+ await locator.waitFor({ state: 'visible', timeout: 60000 });
79
102
 
80
- // Capture current URL to detect SPA navigation
81
- const currentUrl = page.url();
103
+ const previousURL = page.url();
82
104
 
83
- // Handle both SPA transitions and full page reloads
84
- await Promise.all([
85
- page.waitForURL(urlPattern, { timeout }).catch(() => {}),
86
- page.waitForLoadState('load', { timeout }).catch(() => {}),
87
- locator.click(),
105
+ const [, newURL] = await Promise.all([
106
+ page.waitForURL(
107
+ (url) =>
108
+ url.toString() !== previousURL && urlPattern.test(url.toString()),
109
+ { timeout },
110
+ ),
111
+ locator.click().then(() => page.url()),
88
112
  ]);
89
113
 
90
- // Confirm the URL changed successfully
91
- await expect(page).toHaveURL(urlPattern, { timeout });
92
-
93
- // Optional: log navigation success (helps in CI)
94
- console.log(`✅ Navigation from ${currentUrl} → ${page.url()}`);
114
+ console.log(`✅ Navigation from ${previousURL} ${newURL}`);
95
115
  }