@networkpro/web 1.14.3 → 1.15.0

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +6 -4
  3. package/_redirects +4 -2
  4. package/package.json +3 -4
  5. package/scripts/testRedirects.js +84 -0
  6. package/src/lib/components/RedirectPage.svelte +2 -34
  7. package/src/lib/components/layout/HeaderDefault.svelte +8 -3
  8. package/src/lib/components/layout/HeaderHome.svelte +8 -3
  9. package/src/lib/pages/AboutContent.svelte +1 -1
  10. package/src/lib/pages/LicenseContent.svelte +1 -2
  11. package/src/lib/pages/PrivacyDashboard.svelte +0 -1
  12. package/src/lib/styles/css/default.css +22 -0
  13. package/src/lib/styles/global.min.css +1 -1
  14. package/src/lib/utils/redirect.js +52 -0
  15. package/src/lib/utils/utm.js +1 -0
  16. package/src/routes/consultation/+page.svelte +16 -2
  17. package/src/routes/contact/+page.svelte +2 -4
  18. package/src/routes/links/+page.svelte +2 -4
  19. package/src/routes/posts/+page.svelte +2 -4
  20. package/src/routes/privacy-rights/+page.svelte +2 -4
  21. package/tests/unit/{unregisterServiceWorker.test.js → client/lib/unregisterServiceWorker.test.js} +2 -2
  22. package/tests/unit/client/lib/utils/redirect.test.js +80 -0
  23. package/tests/unit/{utm.test.js → client/lib/utils/utm.test.js} +1 -1
  24. package/tests/unit/{routes → client/routes}/page.svelte.test.js +2 -2
  25. package/tests/unit/{checkEnv.test.js → server/checkEnv.test.js} +2 -2
  26. package/tests/unit/{checkVersions.test.js → server/checkVersions.test.js} +2 -2
  27. package/tests/unit/{csp-report.test.js → server/csp-report.test.js} +2 -2
  28. package/tests/{internal → unit/server/internal}/auditCoverage.test.js +13 -6
  29. package/tests/unit/{lib → server/lib}/utils/purify.test.js +2 -2
  30. package/vitest.config.client.js +1 -4
  31. package/vitest.config.server.js +1 -1
package/CHANGELOG.md CHANGED
@@ -22,6 +22,54 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
22
22
 
23
23
  ---
24
24
 
25
+ ## [1.15.0] - 2025-07-01
26
+
27
+ ### Added
28
+
29
+ - `redirect.js` utility to handle browser-aware redirects with fallback logic for Firefox.
30
+ - Unit test for `redirect.js` under `tests/unit/client/lib/utils/redirect.test.js`.
31
+ - `/consultation` redirect route to `utm.js` UTM-tracking logic.
32
+ - Redirect from `/foss` to `/foss-spotlight` in `_redirects`.
33
+ - `scripts/testRedirect.js` to verify Netlify/SvelteKit trailing slash redirect behavior.
34
+ - `test:redirects` script in package.json to trigger `scripts/testRedirects.js`
35
+ - Logic to suppress `rel="noopener noreferrer"` on internal redirects in `HeaderHome.svelte` and `HeaderDefault.svelte`
36
+ - `redirect` flag to navigation metadata to distinguish internal redirect behavior in `HeaderHome.svelte` and `HeaderDefault.svelte`
37
+
38
+ ### Changed
39
+
40
+ - Bumped version to **v1.15.0**
41
+ - Restructured unit tests:
42
+ - Moved `purify.test.js` to `tests/unit/client/lib/utils/`
43
+ - Moved `utm.test.js` to `tests/unit/client/lib/utils/`
44
+ - Moved `unregisterServiceWorker.test.js` to `tests/unit/client/lib/utils/`
45
+ - Moved `page.svelte.test.js` to `tests/unit/client/routes/`
46
+ - Moved `checkEnv.test.js`, `checkVersions.test.js`, and `csp-report.test.js` to `tests/unit/server/`
47
+ - Moved `auditCoverage.test.js` to `tests/unit/server/internal/`
48
+ - Refactored `_redirects` file:
49
+ - Removed trailing slashes to match SvelteKit/Netlify conventions.
50
+ - Updated `RedirectPage.svelte` to use `redirectWithBrowserAwareness()` for better cross-browser redirect behavior.
51
+ - Refactored all relevant `+page.svelte` files to remove local redirect timeouts and centralize logic in `RedirectPage`.
52
+ - Updated `vitest.config.client.js` and `vitest.config.server.js` to:
53
+ - Reflect new directory structure
54
+ - Properly assign `jsdom` for client-side tests and `node` for server-side tests
55
+ - `/consultation`, `/contact`, `/links`, `/posts`, and `/privacy-rights` `+page.svelte` files updated to capture UTM parameters in a privacy-preserving manner.
56
+ - Moved inline styles from `RedirectPage.svelte` to `src/lib/styles/css/default.css`, including `@keyframes spin` animation used by `.loading-spinner`
57
+ - Removed unnecessary `rel` attribute from internal links in `AboutContent.svelte`, `LicenseContent.svelte`, and `PrivacyDashboard.svelte`
58
+ - Updated project README with revised directory structure reflecting separate client/server test folders.
59
+ - Upgraded dependencies:
60
+ - `globals` `^16.2.0` → `^16.3.0`
61
+
62
+ ### Fixed
63
+
64
+ - Firefox-specific issue where delayed `window.location.replace()` triggered a new tab instead of redirecting in the same window — now handled by bypassing the delay in Firefox.
65
+ - Prevented server-context tests from breaking due to `window` usage by scoping them to client-only environments.
66
+
67
+ ## Removed
68
+
69
+ - `head:flatten` and `head:validate` scripts in package.json, as the `_headers` file has been deprecated
70
+
71
+ ---
72
+
25
73
  ## [1.14.3] - 2025-06-30
26
74
 
27
75
  ### Added
@@ -32,6 +80,7 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
32
80
 
33
81
  ### Changed
34
82
 
83
+ - Bumped version to **v1.14.3**
35
84
  - Refactored redirect logic in multiple pages to integrate UTM-aware analytics
36
85
  - All redirect pages now consistently open in a new browser tab using `<a>` fallback
37
86
  - Enhanced `utm.js` logic to support campaign identification for `/contact`, `/links`, `/posts`, and `/privacy-rights`
@@ -566,8 +615,8 @@ This project attempts to follow [Keep a Changelog](https://keepachangelog.com/en
566
615
 
567
616
  <!-- Link references -->
568
617
 
569
- [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.14.3...HEAD
570
- [1.14.3]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.3
618
+ [Unreleased]: https://github.com/netwk-pro/netwk-pro.github.io/compare/v1.15.0...HEAD
619
+ [1.15.0]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.15.0
571
620
  [1.14.2]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.2
572
621
  [1.14.1]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.1
573
622
  [1.14.0]: https://github.com/netwk-pro/netwk-pro.github.io/releases/tag/v1.14.0
package/README.md CHANGED
@@ -92,10 +92,12 @@ This project follows the principles of [Keep a Changelog](https://keepachangelog
92
92
  │ ├── robots.txt # SEO: allow/disallow crawlers
93
93
  │ └── sitemap.xml # SEO: full site map
94
94
  ├── tests/
95
- │ ├── e2e/ # Playwright end-to-end tests
96
- ├── internal/ # Internal audit/test helpers
97
- │ └── auditCoverage.test.js # Warns about untested source modules
98
- └── unit/ # Vitest unit tests
95
+ │ ├── e2e/ # Playwright end-to-end tests
96
+ └── unit/ # Vitest unit tests
97
+ ├── client/ # Client-side (jsdom) unit tests
98
+ ├── server/ # Server-side (node) unit tests
99
+ │ │ └── internal/ # Internal audit/test helpers
100
+ │ │ └── auditCoverage.test.js # Warns about untested source modules
99
101
  ├── _redirects # Netlify redirect rules
100
102
  ├── CHANGELOG.md # Chronological record of notable project changes
101
103
  ├── netlify.toml # Netlify configuration
package/_redirects CHANGED
@@ -3,13 +3,15 @@ https://www.netwk.pro/* https://netwk.pro/:splat 301
3
3
 
4
4
  # Redirect /privacy-policy (and variants) to /privacy
5
5
  /privacy-policy /privacy 301
6
- /privacy-policy/ /privacy 301
7
6
  /privacy-policy/* /privacy/:splat 301
8
7
 
9
8
  # Redirect /legal (and variants) to /license
10
9
  /legal /license 301
11
- /legal/ /license 301
12
10
  /legal/* /license/:splat 301
13
11
 
12
+ # Redirect /foss (and variants) to /foss-spotlight
13
+ /foss /foss-spotlight 301
14
+ /foss/* /foss-spotlight/:splat 301
15
+
14
16
  # Redirect for Netlify function proxy
15
17
  /api/* /.netlify/functions/:splat 200
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@networkpro/web",
3
3
  "private": false,
4
- "version": "1.14.3",
4
+ "version": "1.15.0",
5
5
  "description": "Locking Down Networks, Unlocking Confidence™ | Security, Networking, Privacy — Network Pro Strategies",
6
6
  "keywords": [
7
7
  "advisory",
@@ -60,6 +60,7 @@
60
60
  "test:watch": "vitest --config vitest.config.client.js --watch",
61
61
  "test:coverage": "npm run test:client -- --run --coverage && npm run test:server -- --run --coverage",
62
62
  "test:e2e": "npx playwright test --retries=1",
63
+ "test:redirects": "node scripts/testRedirects.js",
63
64
  "coverage:client": "node scripts/openReport.js client",
64
65
  "coverage:server": "node scripts/openReport.js server",
65
66
  "coverage:open": "npm run coverage:client && npm run coverage:server",
@@ -74,8 +75,6 @@
74
75
  "lhci": "lhci",
75
76
  "lhci:run": "lhci autorun --config=.lighthouserc.cjs",
76
77
  "audit:coverage": "vitest run tests/internal/auditCoverage.test.js",
77
- "head:flatten": "node scripts/flattenHeaders.js",
78
- "head:validate": "node scripts/validateHeaders.js",
79
78
  "postinstall": "npm run check:node"
80
79
  },
81
80
  "dependencies": {
@@ -101,7 +100,7 @@
101
100
  "eslint-config-prettier": "^10.1.5",
102
101
  "eslint-plugin-jsdoc": "^51.3.1",
103
102
  "eslint-plugin-svelte": "^3.10.1",
104
- "globals": "^16.2.0",
103
+ "globals": "^16.3.0",
105
104
  "jsdom": "^26.1.0",
106
105
  "lightningcss": "^1.30.1",
107
106
  "markdownlint": "^0.38.0",
@@ -0,0 +1,84 @@
1
+ /* ==========================================================================
2
+ scripts/testRedirects.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 testRedirects.js
11
+ * @description Tests Netlify redirects against actual endpoints to verify
12
+ * trailing-slash behavior.
13
+ *
14
+ * @module scripts/
15
+ * @author SunDevil311
16
+ * @updated 2025-07-01
17
+ */
18
+
19
+ import https from 'https';
20
+ import { URL } from 'url';
21
+
22
+ /**
23
+ * @typedef {object} RedirectTest
24
+ * @property {string} from - The source URL to test.
25
+ * @property {string} to - The expected destination URL.
26
+ */
27
+
28
+ /** @type {RedirectTest[]} */
29
+ const urls = [
30
+ { from: 'https://netwk.pro/privacy-policy', to: 'https://netwk.pro/privacy' },
31
+ {
32
+ from: 'https://netwk.pro/privacy-policy/',
33
+ to: 'https://netwk.pro/privacy',
34
+ },
35
+ { from: 'https://netwk.pro/foss', to: 'https://netwk.pro/foss-spotlight' },
36
+ { from: 'https://netwk.pro/foss/', to: 'https://netwk.pro/foss-spotlight' },
37
+ {
38
+ from: 'https://www.netwk.pro/foss',
39
+ to: 'https://netwk.pro/foss-spotlight',
40
+ },
41
+ ];
42
+
43
+ /**
44
+ * Tests a single redirect by making a GET request and checking the status code and location header.
45
+ * @param {RedirectTest} redirect - The redirect configuration to test.
46
+ * @returns {Promise<boolean>} - Resolves to true if the redirect is correct, false otherwise.
47
+ */
48
+ function testRedirect({ from, to }) {
49
+ return new Promise((resolve) => {
50
+ const req = https.request(new URL(from), { method: 'GET' }, (res) => {
51
+ const location = res.headers.location;
52
+ const expectedPath = new URL(to).pathname;
53
+
54
+ if (res.statusCode === 301 && location === expectedPath) {
55
+ console.log(`✅ ${from} → ${location}`);
56
+ resolve(true);
57
+ } else {
58
+ console.error(
59
+ `❌ ${from} → Expected 301 to ${expectedPath}, got ${res.statusCode} to ${location}`,
60
+ );
61
+ resolve(false);
62
+ }
63
+ });
64
+
65
+ req.on('error', (err) => {
66
+ console.error(`❌ ${from} → Network error: ${err.message}`);
67
+ resolve(false);
68
+ });
69
+
70
+ req.end();
71
+ });
72
+ }
73
+
74
+ /**
75
+ * Runs all redirect tests and exits the process with a status code reflecting success or failure.
76
+ * @returns {Promise<void>}
77
+ */
78
+ const runTests = async () => {
79
+ const results = await Promise.all(urls.map(testRedirect));
80
+ const failed = results.filter((r) => !r).length;
81
+ process.exit(failed > 0 ? 1 : 0);
82
+ };
83
+
84
+ runTests();
@@ -8,6 +8,7 @@ This file is part of Network Pro.
8
8
 
9
9
  <script>
10
10
  import { onMount } from 'svelte';
11
+ import { redirectWithBrowserAwareness } from '$lib/utils/redirect.js';
11
12
  import FullWidthSection from '$lib/components/FullWidthSection.svelte';
12
13
 
13
14
  export let to;
@@ -15,16 +16,7 @@ This file is part of Network Pro.
15
16
  export let delay = 3;
16
17
 
17
18
  onMount(() => {
18
- if (!to) {
19
- console.warn('⛔ No redirect target provided');
20
- return;
21
- }
22
-
23
- console.log('✅ Starting redirect to:', to);
24
-
25
- setTimeout(() => {
26
- window.location.href = to;
27
- }, delay * 1000);
19
+ redirectWithBrowserAwareness(to, delay);
28
20
  });
29
21
  </script>
30
22
 
@@ -47,27 +39,3 @@ This file is part of Network Pro.
47
39
  </FullWidthSection>
48
40
 
49
41
  <!-- END MAIN CONTENT -->
50
-
51
- <style>
52
- .redirect-content {
53
- text-align: center;
54
- font-family: system-ui, sans-serif;
55
- margin-top: 2rem; /* adjust as needed */
56
- }
57
-
58
- .loading-spinner {
59
- width: 48px;
60
- height: 48px;
61
- margin: 2rem auto;
62
- border: 4px solid #ddd;
63
- animation: spin 1s linear infinite;
64
- border-radius: 50%;
65
- border-top: 4px solid #ffc627;
66
- }
67
-
68
- @keyframes spin {
69
- to {
70
- transform: rotate(360deg);
71
- }
72
- }
73
- </style>
@@ -22,6 +22,8 @@ This file is part of Network Pro.
22
22
  const lhubLink = `${base}/links`;
23
23
  const fossLink = `${base}/foss-spotlight`;
24
24
  const blogLink = 'https://blog.netwk.pro';
25
+ const discussLink =
26
+ 'https://github.com/netwk-pro/netwk-pro.github.io/discussions';
25
27
 
26
28
  /**
27
29
  * Navigation link object.
@@ -30,6 +32,7 @@ This file is part of Network Pro.
30
32
  * @property {string} href - The URL or anchor the link points to.
31
33
  * @property {string} target - Specifies where to open the link (e.g., "_self" or "_blank").
32
34
  * @property {boolean} external - Whether the link points to an external resource.
35
+ * @property {boolean} [redirect=false] - Indicates whether the external link is actually an internal redirect.
33
36
  */
34
37
 
35
38
  /**
@@ -48,15 +51,17 @@ This file is part of Network Pro.
48
51
  },
49
52
  {
50
53
  label: 'discussions',
51
- href: 'https://github.com/netwk-pro/netwk-pro.github.io/discussions',
54
+ href: discussLink,
52
55
  target: PAGE.BLANK,
53
56
  external: true,
57
+ redirect: false,
54
58
  },
55
59
  {
56
60
  label: 'link hub',
57
61
  href: lhubLink,
58
62
  target: PAGE.BLANK,
59
63
  external: true,
64
+ redirect: true,
60
65
  },
61
66
  ];
62
67
 
@@ -69,8 +74,8 @@ This file is part of Network Pro.
69
74
 
70
75
  <!-- BEGIN DEFAULT HEADER -->
71
76
  <nav class="center-nav" aria-label="Site navigation">
72
- {#each nav as { label, href, target, external }, index}
73
- <a {href} {target} rel={external ? rel : undefined}>
77
+ {#each nav as { label, href, target, external, redirect }, index}
78
+ <a {href} {target} rel={!redirect && external ? rel : undefined}>
74
79
  {label}
75
80
  </a>
76
81
  {#if external}
@@ -21,6 +21,8 @@ This file is part of Network Pro.
21
21
  const fossLink = `${base}/foss-spotlight`;
22
22
  const lhubLink = `${base}/links`;
23
23
  const blogLink = 'https://blog.netwk.pro';
24
+ const discussLink =
25
+ 'https://github.com/netwk-pro/netwk-pro.github.io/discussions';
24
26
 
25
27
  /**
26
28
  * Navigation link object.
@@ -29,6 +31,7 @@ This file is part of Network Pro.
29
31
  * @property {string} href - The URL or anchor the link points to.
30
32
  * @property {string} target - Specifies where to open the link (e.g., "_self" or "_blank").
31
33
  * @property {boolean} external - Whether the link points to an external resource.
34
+ * @property {boolean} [redirect=false] - Indicates whether the external link is actually an internal redirect.
32
35
  */
33
36
 
34
37
  /**
@@ -46,15 +49,17 @@ This file is part of Network Pro.
46
49
  },
47
50
  {
48
51
  label: 'discussions',
49
- href: 'https://github.com/netwk-pro/netwk-pro.github.io/discussions',
52
+ href: discussLink,
50
53
  target: PAGE.BLANK,
51
54
  external: true,
55
+ redirect: false,
52
56
  },
53
57
  {
54
58
  label: 'link hub',
55
59
  href: lhubLink,
56
60
  target: PAGE.BLANK,
57
61
  external: true,
62
+ redirect: true,
58
63
  },
59
64
  ];
60
65
 
@@ -67,8 +72,8 @@ This file is part of Network Pro.
67
72
 
68
73
  <!-- BEGIN HOME HEADER -->
69
74
  <nav class="center-nav" aria-label="Homepage navigation">
70
- {#each nav as { label, href, target, external }, index}
71
- <a {href} {target} rel={external ? rel : undefined}>
75
+ {#each nav as { label, href, target, external, redirect }, index}
76
+ <a {href} {target} rel={!redirect && external ? rel : undefined}>
72
77
  {label}
73
78
  </a>
74
79
  {#if external}
@@ -217,7 +217,7 @@ This file is part of Network Pro.
217
217
  <div class="spacer"></div>
218
218
 
219
219
  <p>
220
- <a rel={PAGE.REL} href={contactLink} target={PAGE.BLANK}>Let's connect</a>
220
+ <a href={contactLink} target={PAGE.BLANK}>Let's connect</a>
221
221
  to discuss how we can help secure and strengthen your business today.
222
222
  </p>
223
223
 
@@ -487,8 +487,7 @@ This file is part of Network Pro.
487
487
  {:else if link.id === 'contact'}
488
488
  <p>
489
489
  The Company can be contacted via our
490
- <a rel={PAGE.REL} href={contactLink} target={PAGE.BLANK}
491
- >Contact Form</a>
490
+ <a href={contactLink} target={PAGE.BLANK}>Contact Form</a>
492
491
  or by email at:<br />
493
492
  📧 <strong>{CONTACT.EMAIL}</strong>
494
493
  </p>
@@ -237,7 +237,6 @@ This file is part of Network Pro.
237
237
  </p>
238
238
  <p>
239
239
  To exercise any of these rights, you may submit a request through our <a
240
- rel={PAGE.REL}
241
240
  href={prightsLink}
242
241
  target={PAGE.BLANK}>Privacy Rights Request Form</a
243
242
  >. Alternatively, you can email us at
@@ -580,3 +580,25 @@ footer .container {
580
580
  text-align: center;
581
581
  margin-top: 4rem;
582
582
  }
583
+
584
+ .redirect-content {
585
+ text-align: center;
586
+ font-family: system-ui, sans-serif;
587
+ margin-top: 2rem;
588
+ }
589
+
590
+ .loading-spinner {
591
+ width: 48px;
592
+ height: 48px;
593
+ margin: 2rem auto;
594
+ border: 4px solid #ddd;
595
+ animation: spin 1s linear infinite;
596
+ border-radius: 50%;
597
+ border-top: 4px solid #ffc627;
598
+ }
599
+
600
+ @keyframes spin {
601
+ to {
602
+ transform: rotate(360deg);
603
+ }
604
+ }
@@ -3,4 +3,4 @@ Copyright © 2025 Network Pro Strategies (Network Pro™)
3
3
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
4
4
  This file is part of Network Pro.
5
5
  ========================================================================== */
6
- html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#111;background-color:#ffc627}a:visited,a:visited:hover{color:#cba557}a:visited:focus,a:visited:focus-visible{color:#111!important}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#cba557}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{overflow-wrap:break-word;resize:none;white-space:normal;word-break:normal;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-family:monospace;font-size:.875rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.pgp-entry{flex-wrap:wrap;align-items:center;gap:2rem;margin-bottom:2rem;display:flex}.pgp-text{flex:2;min-width:250px}.pgp-qr{flex:1;min-width:150px}.obtainium-direct-label{margin:.25rem 0 .75rem;font-weight:700}.obtainium-manual-label{margin-top:.75rem;font-weight:700}.obtainium-img{width:185px;height:55px;margin-bottom:.25rem}.obtainium-margin{margin-left:4px}.obtainium-fa-down{color:#ffc627;margin-left:4px}.obtainium-icon{width:50px;height:50px}.proton-img{width:168px;height:24px}.redirect-text{text-align:center;margin-top:4rem}
6
+ html{-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{margin:.67em 0;font-size:2em}hr{box-sizing:content-box}pre{font-family:monospace;font-size:1em}a{background-color:#0000}abbr[title]{border-bottom:none;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted buttontext}fieldset{padding:.35em .75em .625em}legend{color:inherit;box-sizing:border-box;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}details{display:block}summary{display:list-item}template{display:none}html{color:#222;scroll-behavior:smooth;font-size:1em;line-height:1.4}::-moz-selection{text-shadow:none;background:#191919}::selection{text-shadow:none;background:#191919}hr{border:0;border-top:1px solid #ccc;height:1px;margin:1em 0;padding:0;display:block;overflow:visible}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}body{color:#fafafa;background-color:#191919;margin:10px;font-family:Arial,Helvetica,sans-serif}a{text-decoration:none}a:link{color:#ffc627}a:hover,a:active{color:#ffc627;text-decoration:underline}a:focus{color:#111;background-color:#ffc627}a:visited,a:visited:hover{color:#cba557}a:visited:focus,a:visited:focus-visible{color:#111!important}.hidden,[hidden]{display:none!important}.visually-hidden{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.visually-hidden.focusable:active,.visually-hidden.focusable:focus{clip:auto;width:auto;height:auto;white-space:inherit;margin:0;position:static;overflow:visible}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:"";display:table}.clearfix:after{clear:both}@media print{*,:before,:after{color:#000!important;box-shadow:none!important;text-shadow:none!important;background:#fff!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^=\#]:after,a[href^=javascript\:]:after{content:""}pre{white-space:pre-wrap!important}pre,blockquote{page-break-inside:avoid;border:1px solid #999}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}.full-width-section{background-position:50%;background-size:cover;width:100%;max-width:1920px;margin:0 auto}.container{max-width:1200px;margin:0 auto;padding:0 12px}.readable{max-width:900px;margin:0 auto}header,footer{width:100%}header .container,footer .container{max-width:1200px;margin:0 auto;padding:20px 12px}.gh{border-collapse:collapse;border-spacing:0;margin:0 auto}.gh td,.gh th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.gh .gh-tcell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.gh,.gh col{width:auto!important}.gh-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.soc{border-collapse:collapse;border-spacing:0;margin:0 auto}.soc td,.soc th{border-collapse:collapse;word-break:normal;padding:8px;overflow:hidden}.soc .soc-fa{text-align:center;vertical-align:middle}@media screen and (width<=767px){.soc,.soc col{width:auto!important}.soc-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.foss{border-collapse:collapse;border-spacing:0}.foss td,.foss th{border-collapse:collapse;word-break:normal;padding:10px 5px;overflow:hidden}.foss .foss-cell{text-align:center;vertical-align:middle}@media screen and (width<=767px){.foss,.foss col{width:auto!important}.foss-wrap{-webkit-overflow-scrolling:touch;overflow-x:auto}}.bnav{text-align:center;border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav td,.bnav th{text-align:center;vertical-align:middle;word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav .bnav-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav,.bnav col{width:auto!important}.bnav-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.bnav2{border-collapse:collapse;border-spacing:0;margin:0 auto}.bnav2 td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;font-weight:700;line-height:1.125rem;overflow:hidden}.bnav2 th{word-break:normal;border-style:none;padding:12px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.bnav2 .bnav2-cell{text-align:center;vertical-align:middle;align-content:center}@media screen and (width<=767px){.bnav2,.bnav2 col{width:auto!important}.bnav2-wrap{-webkit-overflow-scrolling:touch;margin:auto 0;overflow-x:auto}}.pgp{border-collapse:collapse;border-spacing:0;margin:0 auto}.pgp td{word-break:normal;border-style:none;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp th{word-break:normal;border:1px solid #000;padding:10px;font-size:.875rem;line-height:1.125rem;overflow:hidden}.pgp .pgp-col1{text-align:right;vertical-align:middle;padding-right:1rem}.pgp .pgp-col2{text-align:left;vertical-align:middle;padding-left:1rem}@media screen and (width<=767px){.pgp,.pgp col{width:auto!important}.pgp-wrap{-webkit-overflow-scrolling:touch;margin:2rem 0 auto;overflow-x:auto}}.logo{margin-left:auto;margin-right:auto;display:block}.index-title1{text-align:center;font-style:italic;font-weight:700}.index-title2{letter-spacing:-.015em;text-align:center;font-variant:small-caps;font-size:1.25rem;line-height:1.625rem}.index1{letter-spacing:-.035em;text-align:center;font-style:italic;font-weight:700;line-height:2.125rem}.index2{letter-spacing:-.035em;text-align:center;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.index3{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem}.index4{letter-spacing:-.035em;text-align:center;font-size:1.5rem;line-height:1.75rem;text-decoration:underline}.subhead{letter-spacing:-.035em;font-variant:small-caps;font-size:1.5rem;line-height:1.75rem}.bold{font-weight:700}.emphasis{font-style:italic}.uline{text-decoration:underline}.bolditalic{font-style:italic;font-weight:700}.bquote{border-left:3px solid #9e9e9e;margin-left:30px;padding-left:10px;font-style:italic}.small-text{font-size:.75rem;line-height:1.125rem}.large-text-center{text-align:center;font-size:1.25rem;line-height:1.75rem}.prewrap{white-space:pre-wrap;display:block}.hr-styled{width:75%;margin:auto}.center-text{text-align:center}.copyright{text-align:center;font-size:.75rem;line-height:1.125rem}.gold{color:#ffc627}.visited{color:#cba557}.goldseparator{color:#ffc627;margin:0 .5rem}.center-nav{text-align:center;padding:5px;font-size:1rem;line-height:1.5rem}.block{overflow-wrap:break-word;resize:none;white-space:normal;word-break:normal;background:0 0;border:none;border-radius:0;outline:none;width:100%;font-family:monospace;font-size:.875rem;line-height:1.125rem}.fingerprint{white-space:pre-line;font-weight:700;display:block}.pgp-image{width:150px;height:150px}.spacer{margin:2rem 0}.separator{margin:0 .5rem}.emoji{margin-right:8px}.headline{margin-bottom:4px;font-style:italic;font-weight:700;display:block}.label{font-family:inherit;font-weight:700}.description{font-family:inherit;font-style:normal;font-weight:400;display:inline}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.pgp-entry{flex-wrap:wrap;align-items:center;gap:2rem;margin-bottom:2rem;display:flex}.pgp-text{flex:2;min-width:250px}.pgp-qr{flex:1;min-width:150px}.obtainium-direct-label{margin:.25rem 0 .75rem;font-weight:700}.obtainium-manual-label{margin-top:.75rem;font-weight:700}.obtainium-img{width:185px;height:55px;margin-bottom:.25rem}.obtainium-margin{margin-left:4px}.obtainium-fa-down{color:#ffc627;margin-left:4px}.obtainium-icon{width:50px;height:50px}.proton-img{width:168px;height:24px}.redirect-text{text-align:center;margin-top:4rem}.redirect-content{text-align:center;margin-top:2rem;font-family:system-ui,sans-serif}.loading-spinner{border:4px solid #ddd;border-top-color:#ffc627;border-radius:50%;width:48px;height:48px;margin:2rem auto;animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}
@@ -0,0 +1,52 @@
1
+ /* ==========================================================================
2
+ src/lib/utils/redirect.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 utm.js
11
+ * @description Determines if the current browser is Firefox and skips redirect
12
+ * visual if true.
13
+ *
14
+ * @module src/lib/utils/
15
+ * @author SunDevil311
16
+ * @updated 2025-07-01
17
+ */
18
+
19
+ /**
20
+ * Checks whether the current browser is Firefox.
21
+ * @returns {boolean} True if the browser is Firefox.
22
+ */
23
+ function isFirefox() {
24
+ return (
25
+ typeof navigator !== 'undefined' &&
26
+ navigator.userAgent.toLowerCase().includes('firefox')
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Redirects to a URL, immediately in Firefox (to avoid popup heuristics),
32
+ * and with a visual delay in all other browsers.
33
+ *
34
+ * @param {string} to - Destination URL
35
+ * @param {number} delay - Delay in seconds (ignored for Firefox)
36
+ */
37
+ export function redirectWithBrowserAwareness(to, delay = 2) {
38
+ if (!to) {
39
+ console.warn('⛔ No redirect target provided');
40
+ return;
41
+ }
42
+
43
+ if (isFirefox()) {
44
+ console.log('🦊 Firefox detected — redirecting immediately');
45
+ window.location.replace(to);
46
+ } else {
47
+ console.log(`✅ Delayed redirect to: ${to} after ${delay}s`);
48
+ setTimeout(() => {
49
+ window.location.replace(to);
50
+ }, delay * 1000);
51
+ }
52
+ }
@@ -34,6 +34,7 @@ export function appendUTM(url) {
34
34
  else if (pathname.startsWith('/links')) campaign = 'links';
35
35
  else if (pathname.startsWith('/posts')) campaign = 'posts';
36
36
  else if (pathname.startsWith('/privacy-rights')) campaign = 'prights';
37
+ else if (pathname.startsWith('/consultation')) campaign = 'consult';
37
38
  // add more if needed
38
39
 
39
40
  const utmParams = new URLSearchParams({
@@ -9,6 +9,10 @@ This file is part of Network Pro.
9
9
  <script>
10
10
  import RedirectPage from '$lib/components/RedirectPage.svelte';
11
11
  import { appendUTM } from '$lib/utils/utm.js';
12
+ import { getUTMParams } from '$lib/utils/getUTMParams.js';
13
+ import { trackingEnabled } from '$lib/stores/trackingPreferences';
14
+ import posthog from 'posthog-js';
15
+ import { get } from 'svelte/store';
12
16
  import { onMount } from 'svelte';
13
17
  import { browser } from '$app/environment';
14
18
 
@@ -21,10 +25,20 @@ This file is part of Network Pro.
21
25
  onMount(() => {
22
26
  if (!browser) return;
23
27
 
24
- target = appendUTM(
28
+ const url = appendUTM(
25
29
  'https://cloud.neteng.pro/index.php/apps/appointments/pub/8clCqQrt3AtGbNrr/form',
26
30
  );
27
- show = true;
31
+
32
+ if (get(trackingEnabled)) {
33
+ const utm = getUTMParams(url);
34
+ posthog.capture('redirect_to_consult', {
35
+ target_url: url,
36
+ ...utm,
37
+ });
38
+ }
39
+
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
28
42
  });
29
43
  </script>
30
44
 
@@ -37,10 +37,8 @@ This file is part of Network Pro.
37
37
  });
38
38
  }
39
39
 
40
- setTimeout(() => {
41
- target = url;
42
- show = true;
43
- }, 150);
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
44
42
  });
45
43
  </script>
46
44
 
@@ -40,10 +40,8 @@ This file is part of Network Pro.
40
40
  });
41
41
  }
42
42
 
43
- setTimeout(() => {
44
- target = url;
45
- show = true;
46
- }, 150);
43
+ target = url;
44
+ show = true; // Immediately show RedirectPage
47
45
  });
48
46
  </script>
49
47
 
@@ -40,10 +40,8 @@ This file is part of Network Pro.
40
40
  });
41
41
  }
42
42
 
43
- setTimeout(() => {
44
- target = url;
45
- show = true;
46
- }, 150);
43
+ target = url;
44
+ show = true; // Immediately show RedirectPage
47
45
  });
48
46
  </script>
49
47
 
@@ -37,10 +37,8 @@ This file is part of Network Pro.
37
37
  });
38
38
  }
39
39
 
40
- setTimeout(() => {
41
- target = url;
42
- show = true;
43
- }, 150);
40
+ target = url;
41
+ show = true; // Immediately show RedirectPage
44
42
  });
45
43
  </script>
46
44
 
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/unregisterServiceWorker.test.js
2
+ tests/unit/client/lib/unregisterServiceWorker.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -7,7 +7,7 @@ This file is part of Network Pro.
7
7
  ========================================================================== */
8
8
 
9
9
  import { beforeEach, describe, expect, it, vi } from 'vitest';
10
- import { unregisterServiceWorker } from '../../src/lib/unregisterServiceWorker.js';
10
+ import { unregisterServiceWorker } from '../../../../src/lib/unregisterServiceWorker.js';
11
11
 
12
12
  describe('unregisterServiceWorker()', () => {
13
13
  beforeEach(() => {
@@ -0,0 +1,80 @@
1
+ /* ==========================================================================
2
+ tests/unit/client/lib/utils/redirect.test.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 redirect.test.js
11
+ * @description Unit test for src/lib/utils/redirect.js
12
+ * @module tests/unit/lib/util/
13
+ * @author SunDevil311
14
+ * @updated 2025-07-01
15
+ */
16
+
17
+ import { redirectWithBrowserAwareness } from '$lib/utils/redirect.js';
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
+
20
+ describe('redirectWithBrowserAwareness', () => {
21
+ let originalLocation;
22
+ let originalNavigator;
23
+
24
+ /** @type {{ url: string; delay: number }} */
25
+ const commonMocks = {
26
+ url: 'https://example.com',
27
+ delay: 1,
28
+ };
29
+
30
+ beforeEach(() => {
31
+ // Preserve globals
32
+ originalLocation = window.location;
33
+ originalNavigator = global.navigator;
34
+
35
+ // Stub window.location.replace
36
+ delete window.location;
37
+ window.location = {
38
+ replace: vi.fn(),
39
+ };
40
+ });
41
+
42
+ afterEach(() => {
43
+ // Restore globals
44
+ window.location = originalLocation;
45
+ global.navigator = originalNavigator;
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it('should redirect immediately in Firefox', () => {
50
+ global.navigator = {
51
+ userAgent:
52
+ 'Mozilla/5.0 (Windows NT 10.0; rv:99.0) Gecko/20100101 Firefox/99.0',
53
+ };
54
+
55
+ redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
56
+
57
+ expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
58
+ });
59
+
60
+ it('should redirect after delay in non-Firefox', () => {
61
+ vi.useFakeTimers();
62
+
63
+ global.navigator = {
64
+ userAgent:
65
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0 Safari/537.36',
66
+ };
67
+
68
+ redirectWithBrowserAwareness(commonMocks.url, commonMocks.delay);
69
+
70
+ expect(window.location.replace).not.toHaveBeenCalled();
71
+
72
+ vi.advanceTimersByTime(commonMocks.delay * 1000);
73
+
74
+ expect(window.location.replace).toHaveBeenCalledWith(commonMocks.url);
75
+
76
+ vi.useRealTimers();
77
+ });
78
+ });
79
+
80
+ // cspell:ignore khtml
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/utm.test.js
2
+ tests/unit/client/lib/utils/utm.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/routes/page.svelte.test.js
2
+ tests/unit/client/routes/page.svelte.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -9,7 +9,7 @@ This file is part of Network Pro.
9
9
  import '@testing-library/jest-dom/vitest';
10
10
  import { render, screen } from '@testing-library/svelte';
11
11
  import { describe, expect, test } from 'vitest';
12
- import Page from '../../../src/routes/+page.svelte';
12
+ import Page from '../../../../src/routes/+page.svelte';
13
13
 
14
14
  describe('/+page.svelte', () => {
15
15
  test('should render the home page section', () => {
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/checkEnv.test.js
2
+ tests/unit/server/checkEnv.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -11,7 +11,7 @@ This file is part of Network Pro.
11
11
  */
12
12
 
13
13
  import { afterEach, describe, expect, it } from 'vitest';
14
- import { checkEnv } from '../../scripts/checkEnv.js';
14
+ import { checkEnv } from '../../../scripts/checkEnv.js';
15
15
 
16
16
  describe('checkEnv()', () => {
17
17
  const originalEnv = process.env.ENV_MODE;
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/checkVersions.test.js
2
+ tests/unit/server/checkVersions.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -11,7 +11,7 @@ This file is part of Network Pro.
11
11
  */
12
12
 
13
13
  import { describe, expect, it } from 'vitest';
14
- import { checkVersions } from '../../scripts/checkVersions.js';
14
+ import { checkVersions } from '../../../scripts/checkVersions.js';
15
15
 
16
16
  describe('checkVersions()', () => {
17
17
  it('should match current Node and NPM versions to engine ranges', () => {
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/csp-report.test.js
2
+ tests/unit/server/csp-report.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -18,7 +18,7 @@ This file is part of Network Pro.
18
18
  /** @typedef {import('vitest').TestContext} TestContext */
19
19
 
20
20
  import { beforeEach, describe, expect, it, vi } from 'vitest';
21
- import handler from '../../netlify/edge-functions/csp-report.js';
21
+ import handler from '../../../netlify/edge-functions/csp-report.js';
22
22
 
23
23
  // 🧪 Mock fetch used by sendToNtfy inside the Edge Function
24
24
  global.fetch = vi.fn(() =>
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/internal/auditCoverage.test.js
2
+ tests/unit/server/internal/auditCoverage.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -8,10 +8,11 @@ This file is part of Network Pro.
8
8
 
9
9
  /**
10
10
  * @file auditCoverage.test.js
11
- * @description Scans all .js files in src/ and scripts/ for matching unit test
11
+ * @description Scans all .js files in src/ and scripts/ for matching unit
12
+ * tests
12
13
  * @module tests/internal
13
14
  * @author SunDevil311
14
- * @updated 2025-06-01
15
+ * @updated 2025-07-01
15
16
  */
16
17
 
17
18
  import fs from 'fs';
@@ -60,20 +61,26 @@ describe('auditCoverage', () => {
60
61
  const allFiles = [...srcFiles, ...scriptsFiles].map((f) =>
61
62
  path
62
63
  .relative(process.cwd(), f)
63
- .replace(/\\/g, '/') // Normalize Windows slashes
64
+ .replace(/\\/g, '/')
64
65
  .replace(/^src\//, '')
65
66
  .replace(/^scripts\//, '')
66
67
  .replace(/\.js$/, ''),
67
68
  );
68
69
 
69
- const testFiles = getAllJsFiles(path.resolve('tests/unit'), {
70
+ const clientTests = getAllJsFiles(path.resolve('tests/unit/client'), {
71
+ includeTests: true,
72
+ });
73
+ const serverTests = getAllJsFiles(path.resolve('tests/unit/server'), {
70
74
  includeTests: true,
71
75
  });
76
+ const testFiles = [...clientTests, ...serverTests];
72
77
 
73
78
  const testFilesNormalized = testFiles.map((f) =>
74
79
  path
75
- .relative(path.resolve('tests/unit'), f)
80
+ .relative(process.cwd(), f)
76
81
  .replace(/\\/g, '/')
82
+ .replace(/^tests\/unit\/client\//, '')
83
+ .replace(/^tests\/unit\/server\//, '')
77
84
  .replace(/\.test\.js$|\.spec\.js$/, ''),
78
85
  );
79
86
 
@@ -1,5 +1,5 @@
1
1
  /* ==========================================================================
2
- tests/unit/lib/utils/purify.test.js
2
+ tests/unit/server/lib/utils/purify.test.js
3
3
 
4
4
  Copyright © 2025 Network Pro Strategies (Network Pro™)
5
5
  SPDX-License-Identifier: CC-BY-4.0 OR GPL-3.0-or-later
@@ -15,7 +15,7 @@ This file is part of Network Pro.
15
15
  */
16
16
 
17
17
  import { describe, expect, it } from 'vitest';
18
- import { sanitizeHtml } from '../../../../src/lib/utils/purify.js';
18
+ import { sanitizeHtml } from '../../../../../src/lib/utils/purify.js';
19
19
 
20
20
  describe('sanitizeHtml', () => {
21
21
  it('removes dangerous tags like <script>', async () => {
@@ -26,10 +26,7 @@ export default defineConfig({
26
26
  name: 'client',
27
27
  environment: 'jsdom',
28
28
  clearMocks: true,
29
- include: [
30
- 'tests/unit/**/*.test.{js,mjs,svelte}',
31
- 'tests/internal/**/*.test.{js,mjs,svelte}',
32
- ],
29
+ include: ['tests/unit/client/**/*.test.{js,mjs,svelte}'],
33
30
  exclude: [],
34
31
  setupFiles: ['./vitest-setup-client.js'],
35
32
  reporters: ['default', 'json'],
@@ -23,7 +23,7 @@ export default defineConfig({
23
23
  test: {
24
24
  name: 'server',
25
25
  environment: 'node',
26
- include: ['tests/unit/**/*.test.{js,mjs}'],
26
+ include: ['tests/unit/server/**/*.test.{js,mjs}'],
27
27
  exclude: ['tests/unit/**/*.svelte.test.{js,mjs}'],
28
28
  reporters: ['default', 'json'],
29
29
  testTimeout: 10000,