@networkpro/web 1.7.0 → 1.7.2
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/README.md +57 -13
- package/package.json +5 -5
- package/static/robots.txt +2 -1
- package/tests/e2e/app.spec.js +40 -16
- package/tests/e2e/mobile.spec.js +32 -42
- package/tests/e2e/shared/helpers.js +57 -0
package/README.md
CHANGED
|
@@ -31,10 +31,14 @@ All infrastructure and data flows are designed with **maximum transparency, self
|
|
|
31
31
|
```bash
|
|
32
32
|
.
|
|
33
33
|
├── .github/workflows/ # CI workflows and automation
|
|
34
|
-
├── .vscode/
|
|
34
|
+
├── .vscode/
|
|
35
|
+
│ ├── customData.json # Custom CSS data for FontAwesome icons
|
|
36
|
+
│ ├── extensions.json # Recommended VSCodium / VS Code extensions
|
|
37
|
+
│ ├── extensions.jsonc # Commented version of extensions.json for reference
|
|
38
|
+
│ └── settings.json # User settings for VSCodium / VS Code
|
|
35
39
|
├── netlify-functions/
|
|
36
40
|
│ └── cspReport.js # Serverless function to receive and log CSP violation reports
|
|
37
|
-
├── scripts/ #
|
|
41
|
+
├── scripts/ # General utility scripts
|
|
38
42
|
├── src/
|
|
39
43
|
│ ├── lib/ # Reusable components, styles, utilities
|
|
40
44
|
│ ├── routes/ # SvelteKit routes (+page.svelte, +page.server.js)
|
|
@@ -43,13 +47,33 @@ All infrastructure and data flows are designed with **maximum transparency, self
|
|
|
43
47
|
│ ├── app.html # SvelteKit entry HTML with CSP/meta/bootentry
|
|
44
48
|
│ └── service-worker.js # Custom Service Worker
|
|
45
49
|
├── static/ # Static assets served at root
|
|
50
|
+
│ ├── manifest.json # Manifest file for PWA configuration
|
|
51
|
+
│ ├── robots.txt # Instructions for web robots
|
|
52
|
+
│ └── sitemap.xml # Sitemap for search engines
|
|
46
53
|
├── tests/
|
|
47
54
|
│ ├── e2e/ # End-to-end Playwright tests
|
|
48
55
|
│ └── unit/ # Vite unit tests
|
|
56
|
+
├── _redirects # Netlify redirects
|
|
49
57
|
├── netlify.toml # Netlify configuration
|
|
50
58
|
└── ...
|
|
51
59
|
```
|
|
52
60
|
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
### E2E Test Structure
|
|
64
|
+
|
|
65
|
+
End-to-end tests are located in `tests/e2e/` and organized by feature or route:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
tests/
|
|
69
|
+
├── e2e/
|
|
70
|
+
│ ├── app.spec.js # Desktop and mobile route tests
|
|
71
|
+
│ ├── mobile.spec.js # Mobile-specific assertions
|
|
72
|
+
│ └── shared/
|
|
73
|
+
│ └── helpers.js # Shared test utilities (e.g., getFooter, setDesktopView, setMobileView)
|
|
74
|
+
└── ...
|
|
75
|
+
```
|
|
76
|
+
|
|
53
77
|
---
|
|
54
78
|
|
|
55
79
|
## 🛠 Getting Started
|
|
@@ -180,7 +204,7 @@ Located at `src/hooks.server.js`, this file is responsible for injecting dynamic
|
|
|
180
204
|
To re-enable nonce generation for inline scripts in the future:
|
|
181
205
|
|
|
182
206
|
1. Uncomment the nonce generation and injection logic in `hooks.server.js`.
|
|
183
|
-
2. Add `nonce="
|
|
207
|
+
2. Add `nonce="__cspNonce__"` to inline `<script>` blocks in `app.html` or route templates.
|
|
184
208
|
|
|
185
209
|
> 💡 The `[headers]` block in `netlify.toml` has been deprecated — all headers are now set dynamically from within SvelteKit.
|
|
186
210
|
|
|
@@ -244,6 +268,24 @@ This project uses a mix of automated performance, accessibility, and end-to-end
|
|
|
244
268
|
|
|
245
269
|
|
|
246
270
|
|
|
271
|
+
### E2E Setup
|
|
272
|
+
|
|
273
|
+
Playwright is included in `devDependencies` and installed automatically with:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
npm install
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
To install browser dependencies (required once):
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
npx playwright install
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
> This downloads the browser binaries (Chromium, Firefox, WebKit) used for testing. You only need to run this once per machine or after a fresh clone.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
247
289
|
### Running Tests
|
|
248
290
|
|
|
249
291
|
Local testing via Vitest and Playwright:
|
|
@@ -254,9 +296,11 @@ npm run test:server # Run server-side unit tests with Vitest
|
|
|
254
296
|
npm run test:all # Run full test suite
|
|
255
297
|
npm run test:watch # Watch mode for client tests
|
|
256
298
|
npm run test:coverage # Collect code coverage reports
|
|
257
|
-
npm run test:e2e # Runs Playwright E2E tests
|
|
299
|
+
npm run test:e2e # Runs Playwright E2E tests (with one retry on failure)
|
|
258
300
|
```
|
|
259
301
|
|
|
302
|
+
> Playwright will retry failed tests once `(--retries=1)` to reduce false negatives from transient flakiness (network, render delay, etc.).
|
|
303
|
+
|
|
260
304
|
Audit your app using Lighthouse:
|
|
261
305
|
|
|
262
306
|
```bash
|
|
@@ -399,15 +443,15 @@ The following CLI commands are available via `npm run <script>` or `pnpm run <sc
|
|
|
399
443
|
|
|
400
444
|
<!-- markdownlint-enable MD024 -->
|
|
401
445
|
|
|
402
|
-
| Script | Description
|
|
403
|
-
| --------------- |
|
|
404
|
-
| `test` | Alias for `test:all`
|
|
405
|
-
| `test:all` | Run both client and server test suites
|
|
406
|
-
| `test:client` | Run client tests with Vitest
|
|
407
|
-
| `test:server` | Run server-side tests with Vitest
|
|
408
|
-
| `test:watch` | Watch mode for client tests
|
|
409
|
-
| `test:coverage` | Collect coverage from both client and server
|
|
410
|
-
| `test:e2e` |
|
|
446
|
+
| Script | Description |
|
|
447
|
+
| --------------- | ------------------------------------------------------ |
|
|
448
|
+
| `test` | Alias for `test:all` |
|
|
449
|
+
| `test:all` | Run both client and server test suites |
|
|
450
|
+
| `test:client` | Run client tests with Vitest |
|
|
451
|
+
| `test:server` | Run server-side tests with Vitest |
|
|
452
|
+
| `test:watch` | Watch mode for client tests |
|
|
453
|
+
| `test:coverage` | Collect coverage from both client and server |
|
|
454
|
+
| `test:e2e` | Runs E2E tests with up to 1 automatic retry on failure |
|
|
411
455
|
|
|
412
456
|
---
|
|
413
457
|
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"sideEffects": [
|
|
5
5
|
"./.netlify/shims.js"
|
|
6
6
|
],
|
|
7
|
-
"version": "1.7.
|
|
7
|
+
"version": "1.7.2",
|
|
8
8
|
"description": "Locking Down Networks, Unlocking Confidence | Security, Networking, Privacy — Network Pro Strategies",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"advisory",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"test:server": "vitest --config vitest.config.server.js",
|
|
61
61
|
"test:watch": "vitest --config vitest.config.client.js --watch",
|
|
62
62
|
"test:coverage": "npm run test:client -- --run --coverage && npm run test:server -- --run --coverage",
|
|
63
|
-
"test:e2e": "npx playwright test",
|
|
63
|
+
"test:e2e": "npx playwright test --retries=1",
|
|
64
64
|
"lint": "eslint . --ext .mjs,.js,.svelte",
|
|
65
65
|
"lint:fix": "eslint . --ext .mjs,.js,.svelte --fix",
|
|
66
66
|
"lint:jsdoc": "eslint . --ext .js,.mjs,.svelte --max-warnings=0",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"nodemailer": "^7.0.3",
|
|
81
81
|
"posthog-js": "^1.248.1",
|
|
82
82
|
"semver": "^7.7.2",
|
|
83
|
-
"svelte": "5.33.
|
|
83
|
+
"svelte": "5.33.10"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@eslint/compat": "^1.2.9",
|
|
@@ -106,10 +106,10 @@
|
|
|
106
106
|
"markdownlint-cli2": "^0.18.1",
|
|
107
107
|
"mdsvex": "^0.12.6",
|
|
108
108
|
"playwright": "^1.52.0",
|
|
109
|
-
"postcss": "^8.5.
|
|
109
|
+
"postcss": "^8.5.4",
|
|
110
110
|
"prettier": "^3.5.3",
|
|
111
111
|
"prettier-plugin-svelte": "^3.4.0",
|
|
112
|
-
"stylelint": "^16.
|
|
112
|
+
"stylelint": "^16.20.0",
|
|
113
113
|
"stylelint-config-html": "^1.1.0",
|
|
114
114
|
"stylelint-config-recommended": "^16.0.0",
|
|
115
115
|
"stylelint-order": "^7.0.0",
|
package/static/robots.txt
CHANGED
package/tests/e2e/app.spec.js
CHANGED
|
@@ -6,8 +6,23 @@ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
|
6
6
|
This file is part of Network Pro.
|
|
7
7
|
========================================================================== */
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @file app.spec.js
|
|
11
|
+
* @description Runs Playwright E2E tests with desktop and root route assertions.
|
|
12
|
+
* @module tests/e2e
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-05-29
|
|
15
|
+
*/
|
|
10
16
|
|
|
17
|
+
import { expect, test } from "@playwright/test";
|
|
18
|
+
import {
|
|
19
|
+
getFooter,
|
|
20
|
+
getVisibleNav,
|
|
21
|
+
setDesktopView,
|
|
22
|
+
setMobileView,
|
|
23
|
+
} from "./shared/helpers.js";
|
|
24
|
+
|
|
25
|
+
// Root route should load successfully with the correct title
|
|
11
26
|
test.describe("Desktop Tests", () => {
|
|
12
27
|
test("should load successfully with the correct title", async ({
|
|
13
28
|
page,
|
|
@@ -15,43 +30,52 @@ test.describe("Desktop Tests", () => {
|
|
|
15
30
|
}) => {
|
|
16
31
|
if (browserName === "webkit") test.skip();
|
|
17
32
|
|
|
18
|
-
await page
|
|
19
|
-
await page.waitForTimeout(1500);
|
|
33
|
+
await setDesktopView(page);
|
|
20
34
|
await page.goto("/");
|
|
21
35
|
await page.waitForLoadState("load", { timeout: 60000 });
|
|
22
36
|
await expect(page).toHaveTitle(/Locking Down Networks/);
|
|
23
37
|
});
|
|
24
38
|
|
|
39
|
+
// Root route should display nav bar and about link
|
|
25
40
|
test("should display the navigation bar and 'about' link", async ({
|
|
26
41
|
page,
|
|
27
42
|
}) => {
|
|
28
|
-
await page
|
|
29
|
-
await page.waitForTimeout(1500);
|
|
43
|
+
await setDesktopView(page);
|
|
30
44
|
await page.goto("/");
|
|
31
45
|
|
|
32
|
-
const nav = page
|
|
33
|
-
await expect(nav).toBeVisible();
|
|
46
|
+
const nav = await getVisibleNav(page);
|
|
34
47
|
|
|
35
48
|
const aboutLink = nav.getByRole("link", { name: "about" });
|
|
36
49
|
await expect(aboutLink).toBeVisible();
|
|
37
50
|
await expect(aboutLink).toHaveAttribute("href", "/about");
|
|
38
51
|
});
|
|
39
52
|
|
|
53
|
+
// Root route should display the footer properly
|
|
40
54
|
test("should display the footer correctly", async ({ page }) => {
|
|
41
|
-
await page
|
|
42
|
-
await page.waitForTimeout(1500);
|
|
55
|
+
await setDesktopView(page);
|
|
43
56
|
await page.goto("/");
|
|
44
57
|
|
|
45
58
|
const footer = page.locator("footer");
|
|
46
59
|
await expect(footer).toBeVisible();
|
|
47
60
|
});
|
|
48
61
|
|
|
62
|
+
// About route should display the footer properly
|
|
63
|
+
test("should render the 'about' page with footer visible", async ({
|
|
64
|
+
page,
|
|
65
|
+
}) => {
|
|
66
|
+
await setDesktopView(page);
|
|
67
|
+
await page.goto("/about");
|
|
68
|
+
|
|
69
|
+
const footer = getFooter(page);
|
|
70
|
+
await expect(footer).toBeVisible();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Root route should have a clickable "about" link
|
|
49
74
|
test("should ensure the 'about' link is clickable", async ({ page }) => {
|
|
50
|
-
await page
|
|
75
|
+
await setDesktopView(page);
|
|
51
76
|
await page.goto("/");
|
|
52
77
|
|
|
53
|
-
const nav = page
|
|
54
|
-
await expect(nav).toBeVisible({ timeout: 60000 });
|
|
78
|
+
const nav = await getVisibleNav(page);
|
|
55
79
|
|
|
56
80
|
const aboutLink = nav.getByRole("link", { name: "about" });
|
|
57
81
|
await expect(aboutLink).toBeVisible({ timeout: 60000 });
|
|
@@ -62,6 +86,7 @@ test.describe("Desktop Tests", () => {
|
|
|
62
86
|
});
|
|
63
87
|
}); // End Desktop Tests
|
|
64
88
|
|
|
89
|
+
// Root route should load successfully with the correct title (mobile)
|
|
65
90
|
test.describe("Mobile Tests", () => {
|
|
66
91
|
test("should load successfully with the correct title on mobile", async ({
|
|
67
92
|
page,
|
|
@@ -69,16 +94,15 @@ test.describe("Mobile Tests", () => {
|
|
|
69
94
|
}) => {
|
|
70
95
|
if (browserName === "webkit") test.skip();
|
|
71
96
|
|
|
72
|
-
await page
|
|
73
|
-
await page.waitForTimeout(1500);
|
|
97
|
+
await setMobileView(page);
|
|
74
98
|
await page.goto("/");
|
|
75
99
|
await page.waitForLoadState("load", { timeout: 60000 });
|
|
76
100
|
await expect(page).toHaveTitle(/Locking Down Networks/);
|
|
77
101
|
});
|
|
78
102
|
|
|
103
|
+
// Root route should display headings properly on mobile
|
|
79
104
|
test("should display main content correctly on mobile", async ({ page }) => {
|
|
80
|
-
await page
|
|
81
|
-
await page.waitForTimeout(1500);
|
|
105
|
+
await setMobileView(page);
|
|
82
106
|
await page.goto("/");
|
|
83
107
|
|
|
84
108
|
const mainHeading = page.locator("h1, h2");
|
package/tests/e2e/mobile.spec.js
CHANGED
|
@@ -6,33 +6,29 @@ SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
|
6
6
|
This file is part of Network Pro.
|
|
7
7
|
========================================================================== */
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @file mobile.spec.js
|
|
11
|
+
* @description Runs Playwright E2E tests with mobile assertions.
|
|
12
|
+
* @module tests/e2e
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-05-29
|
|
15
|
+
*/
|
|
16
|
+
|
|
9
17
|
import { expect, test } from "@playwright/test";
|
|
18
|
+
import { getFooter, getVisibleNav, setMobileView } from "./shared/helpers.js";
|
|
10
19
|
|
|
20
|
+
// Mobile viewport smoke tests for the root route
|
|
11
21
|
test.describe("Mobile Tests", () => {
|
|
12
|
-
// Skip WebKit for mobile tests if it's problematic
|
|
13
22
|
test("should display the main description text on mobile", async ({
|
|
14
23
|
page,
|
|
15
24
|
browserName,
|
|
16
25
|
}) => {
|
|
17
|
-
if (browserName === "webkit")
|
|
18
|
-
test.skip(); // Skip WebKit if it's problematic
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
await page.setViewportSize({ width: 375, height: 667 }); // Mobile size (e.g., iPhone 6)
|
|
22
|
-
|
|
23
|
-
// Add a small timeout before navigating to the page
|
|
24
|
-
await page.waitForTimeout(1500); // Wait for 1.5 seconds
|
|
26
|
+
if (browserName === "webkit") test.skip();
|
|
25
27
|
|
|
28
|
+
await setMobileView(page);
|
|
26
29
|
await page.goto("/");
|
|
27
|
-
|
|
28
|
-
// Wait for the page to load and for the title element to be available
|
|
29
30
|
await page.waitForLoadState("domcontentloaded", { timeout: 60000 });
|
|
30
|
-
await page.waitForSelector(
|
|
31
|
-
'div.index-title1:has-text("Locking Down Networks")',
|
|
32
|
-
{ timeout: 60000 },
|
|
33
|
-
);
|
|
34
31
|
|
|
35
|
-
// Assert that the correct text is found inside the <div>
|
|
36
32
|
const description = page.locator(
|
|
37
33
|
'div.index-title1:has-text("Locking Down Networks")',
|
|
38
34
|
);
|
|
@@ -43,21 +39,12 @@ test.describe("Mobile Tests", () => {
|
|
|
43
39
|
page,
|
|
44
40
|
browserName,
|
|
45
41
|
}) => {
|
|
46
|
-
if (browserName === "webkit")
|
|
47
|
-
test.skip(); // Skip WebKit if it's problematic
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
await page.setViewportSize({ width: 375, height: 667 }); // Mobile size
|
|
51
|
-
|
|
52
|
-
// Add a small timeout before navigating to the page
|
|
53
|
-
await page.waitForTimeout(1500); // Wait for 1.5 seconds
|
|
42
|
+
if (browserName === "webkit") test.skip();
|
|
54
43
|
|
|
44
|
+
await setMobileView(page);
|
|
55
45
|
await page.goto("/");
|
|
56
|
-
|
|
57
|
-
// Wait for the page to load
|
|
58
46
|
await page.waitForLoadState("domcontentloaded", { timeout: 60000 });
|
|
59
47
|
|
|
60
|
-
// Check that the main heading is visible on mobile
|
|
61
48
|
const mainHeading = page.locator("h1, h2");
|
|
62
49
|
await expect(mainHeading).toBeVisible();
|
|
63
50
|
});
|
|
@@ -66,28 +53,31 @@ test.describe("Mobile Tests", () => {
|
|
|
66
53
|
page,
|
|
67
54
|
browserName,
|
|
68
55
|
}) => {
|
|
69
|
-
if (browserName === "webkit")
|
|
70
|
-
test.skip(); // Skip WebKit if it's problematic
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
await page.setViewportSize({ width: 375, height: 667 }); // Mobile size
|
|
74
|
-
|
|
75
|
-
// Add a small timeout before navigating to the page
|
|
76
|
-
await page.waitForTimeout(1500); // Wait for 1.5 seconds
|
|
56
|
+
if (browserName === "webkit") test.skip();
|
|
77
57
|
|
|
58
|
+
await setMobileView(page);
|
|
78
59
|
await page.goto("/");
|
|
79
60
|
|
|
80
|
-
|
|
81
|
-
|
|
61
|
+
const nav = await getVisibleNav(page);
|
|
62
|
+
const aboutLink = nav.getByRole("link", { name: "about" });
|
|
63
|
+
await expect(aboutLink).toBeVisible({ timeout: 60000 });
|
|
82
64
|
|
|
83
|
-
// Ensure the "about" link is visible and clickable
|
|
84
|
-
const aboutLink = page.locator("a", { hasText: "about" });
|
|
85
|
-
await expect(aboutLink).toBeVisible();
|
|
86
65
|
await aboutLink.click();
|
|
87
|
-
|
|
88
|
-
// Assert that it navigates to the correct route
|
|
89
66
|
await expect(page).toHaveURL(/\/about/);
|
|
90
67
|
});
|
|
68
|
+
|
|
69
|
+
test("should display the footer on /about (mobile)", async ({
|
|
70
|
+
page,
|
|
71
|
+
browserName,
|
|
72
|
+
}) => {
|
|
73
|
+
if (browserName === "webkit") test.skip();
|
|
74
|
+
|
|
75
|
+
await setMobileView(page);
|
|
76
|
+
await page.goto("/about");
|
|
77
|
+
|
|
78
|
+
const footer = getFooter(page);
|
|
79
|
+
await expect(footer).toBeVisible();
|
|
80
|
+
});
|
|
91
81
|
});
|
|
92
82
|
|
|
93
83
|
// cspell:ignore domcontentloaded
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
tests/e2e/shared/helpers.js
|
|
3
|
+
|
|
4
|
+
Copyright © 2025 Network Pro Strategies (Network Pro™)
|
|
5
|
+
SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
|
|
6
|
+
This file is part of Network Pro.
|
|
7
|
+
========================================================================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @file helpers.js
|
|
11
|
+
* @description Stores commonly used functions for importing into E2E tests.
|
|
12
|
+
* @module tests/e2e/shared
|
|
13
|
+
* @author SunDevil311
|
|
14
|
+
* @updated 2025-05-29
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
|
19
|
+
* @returns {Promise<void>}
|
|
20
|
+
* @description Sets standard desktop viewport and waits for animations.
|
|
21
|
+
*/
|
|
22
|
+
export async function setDesktopView(page) {
|
|
23
|
+
await page.setViewportSize({ width: 1280, height: 720 });
|
|
24
|
+
await page.waitForTimeout(1500);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {import('@playwright/test').Page} page
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
export async function setMobileView(page) {
|
|
32
|
+
await page.setViewportSize({ width: 375, height: 667 });
|
|
33
|
+
await page.waitForTimeout(1500);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
|
38
|
+
* @returns {Promise<import('@playwright/test').Locator>} - A visible navigation locator.
|
|
39
|
+
* @throws {Error} If no visible navigation is found.
|
|
40
|
+
*/
|
|
41
|
+
export async function getVisibleNav(page) {
|
|
42
|
+
const navHome = page.getByRole("navigation", { name: "Homepage navigation" });
|
|
43
|
+
const navMain = page.getByRole("navigation", { name: "Main navigation" });
|
|
44
|
+
|
|
45
|
+
if (await navHome.isVisible()) return navHome;
|
|
46
|
+
if (await navMain.isVisible()) return navMain;
|
|
47
|
+
|
|
48
|
+
throw new Error("No visible navigation element found.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {import('@playwright/test').Page} page
|
|
53
|
+
* @returns {import('@playwright/test').Locator}
|
|
54
|
+
*/
|
|
55
|
+
export function getFooter(page) {
|
|
56
|
+
return page.locator("footer");
|
|
57
|
+
}
|