Skip to content

Commit 99f3753

Browse files
committed
Add E2E and unit test for site url utilities
1 parent bad591b commit 99f3753

9 files changed

Lines changed: 205 additions & 8 deletions

File tree

@types/window.d.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ interface EnvironmentApiSnapshot {
3232
privacyPolicyVersion: string
3333
}
3434

35+
interface SiteUrlSnapshot {
36+
siteUrl: string
37+
}
38+
3539
declare global {
3640
interface Window {
3741
/**
@@ -55,6 +59,16 @@ declare global {
5559
* Snapshot of environment-api (server helper) results for Playwright assertions
5660
*/
5761
environmentApiSnapshot?: EnvironmentApiSnapshot
62+
63+
/**
64+
* Snapshot of client-side site URL helper
65+
*/
66+
siteUrlClientSnapshot?: SiteUrlSnapshot
67+
68+
/**
69+
* Snapshot of server-side site URL helper
70+
*/
71+
siteUrlApiSnapshot?: SiteUrlSnapshot
5872
}
5973
}
6074

_TODO.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,20 @@
55
1. build Supabase-aware fixtures to cover the GDPR endpoints (consent/request/export/verify) end-to-end
66
2. add integration-style tests that exercise the DSAR flow using a lightweight fake DB layer.
77

8-
Files with Skipped Tests:
8+
## Unit tests failure groups
9+
10+
1. client.spec.ts, Navigation specs, Forms/Download specs, and multiple selector tests crash because document isn't defined or Astro can't render component fixtures. These need a DOM-aware environment (// @vitest-environment happy-dom) plus Container-based fixtures per repo standards.
11+
12+
2. Selector tests in selectors.spec.ts and download form selectors expect thrown errors, but the helper functions currently return [object Object]; check that error helpers throw ClientScriptError (or similar) and ensure mocks expose the ClientScriptError export (see bootstrap tests complaining about missing export).
13+
14+
3. pageTitle.spec.ts and absoluteUrl.spec.ts also show [object Object] instead of the expected message, indicating the helpers throw non-Error values; update them to throw Error (or a typed error) with the asserted message.
15+
16+
4. rateLimit.spec.ts fails because isDev/isTest mocks aren't mocked functions; wrap the @lib/config/environment import with vi.mock and provide spies so .mockReturnValue works.
17+
dsarVerificationEmails.spec.ts can't run because RESEND_API_KEY isn't set. Provide a fake key via process.env.RESEND_API_KEY = 'test' in the suite (and mock Resend client) so tests don't hit real env requirements.
18+
19+
5. componentDiscovery.spec.ts expects an error string but receives an object; ensure the helper throws an Error with the message the test asserts.
20+
21+
## Files with Skipped Tests
922

1023
social-shares.spec.ts - 12 @wip
1124
gdpr-consent.spec.ts - 10 @wip
@@ -254,7 +267,7 @@ https://www.kirilv.com/canvas-confetti/
254267

255268
## @TODO: Use the Page Visibility API to pause videos, image carousels, and animations
256269

257-
Stop unnecessary processes when the user doesnt see the page or inversely to perform background actions.
270+
Stop unnecessary processes when the user doesn't see the page or inversely to perform background actions.
258271

259272
## @TODO: "Add to Calendar" button
260273

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect, vi, afterEach } from 'vitest'
2+
3+
const originalDevServerPort = process.env['DEV_SERVER_PORT']
4+
5+
// Reload the module with a fresh environment snapshot and mocked dependencies.
6+
const importSiteUrlServer = async () => {
7+
vi.resetModules()
8+
const isVercelMock = vi.fn(() => false)
9+
vi.doMock('../environmentServer', () => ({
10+
isVercel: isVercelMock,
11+
}))
12+
vi.doMock('../../../../package.json', () => ({
13+
domain: 'webstackbuilders.com',
14+
default: { domain: 'webstackbuilders.com' },
15+
}))
16+
const module = await import('../siteUrlServer')
17+
return { getSiteUrl: module.getSiteUrl, isVercelMock }
18+
}
19+
20+
afterEach(() => {
21+
if (originalDevServerPort === undefined) {
22+
delete process.env['DEV_SERVER_PORT']
23+
} else {
24+
process.env['DEV_SERVER_PORT'] = originalDevServerPort
25+
}
26+
vi.restoreAllMocks()
27+
})
28+
29+
describe('getSiteUrl', () => {
30+
it('returns the production domain when Vercel runtime is detected', async () => {
31+
const { getSiteUrl, isVercelMock } = await importSiteUrlServer()
32+
isVercelMock.mockReturnValue(true)
33+
expect(getSiteUrl()).toBe('https://webstackbuilders.com')
34+
})
35+
36+
it('uses the provided DEV_SERVER_PORT when not running on Vercel', async () => {
37+
process.env['DEV_SERVER_PORT'] = '8888'
38+
const { getSiteUrl, isVercelMock } = await importSiteUrlServer()
39+
isVercelMock.mockReturnValue(false)
40+
expect(getSiteUrl()).toBe('http://localhost:8888')
41+
})
42+
43+
it('falls back to the default localhost:4321 when DEV_SERVER_PORT is unset', async () => {
44+
delete process.env['DEV_SERVER_PORT']
45+
const { getSiteUrl, isVercelMock } = await importSiteUrlServer()
46+
isVercelMock.mockReturnValue(false)
47+
expect(getSiteUrl()).toBe('http://localhost:4321')
48+
})
49+
50+
it('falls back to the default when DEV_SERVER_PORT is whitespace', async () => {
51+
process.env['DEV_SERVER_PORT'] = ' '
52+
const { getSiteUrl, isVercelMock } = await importSiteUrlServer()
53+
isVercelMock.mockReturnValue(false)
54+
expect(getSiteUrl()).toBe('http://localhost:4321')
55+
})
56+
})

src/pages/api/_environment/environmentApi.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export {
1818
isVercel,
1919
} from '@lib/config/environmentServer'
2020

21-
//export { getSiteUrl } from '@lib/config/siteUrlServer'
22-
2321
/**
2422
* Privacy Policy Version Utility
2523
*

src/pages/api/_environment/siteUrlApi.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@
22
* Server-side method to determine correct URL
33
*/
44

5-
// @TODO: package.json isn't available from SSR serverless functions !!!!
6-
75
import packageJson from '../../../../package.json' with { type: 'json' }
8-
import { isVercel } from './'
6+
import { isVercel } from './environmentApi'
97

10-
const { domain } = packageJson
118
const devServerPort = process.env['DEV_SERVER_PORT']?.trim()
129
const resolvedDevServerPort = devServerPort && devServerPort.length > 0 ? devServerPort : '4321'
10+
const { domain } = packageJson
1311

1412
/** Called from astro.config.ts to determine "site" config key */
1513
export const getSiteUrl = (): string => {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
import { getSiteUrl } from '@pages/api/_environment/siteUrlApi'
3+
4+
const snapshot = {
5+
siteUrl: getSiteUrl(),
6+
}
7+
8+
const formattedSnapshot = JSON.stringify(snapshot, null, 2)
9+
const scriptContent = `window.siteUrlApiSnapshot = ${JSON.stringify(snapshot).replace(/</g, '\\u003c')}`
10+
---
11+
12+
<!DOCTYPE html>
13+
<html lang="en">
14+
<head>
15+
<meta charset="utf-8">
16+
<title>Site URL API Diagnostics</title>
17+
<meta name="robots" content="noindex, nofollow">
18+
</head>
19+
<body>
20+
<main>
21+
<h1>Site URL API Diagnostics</h1>
22+
<p data-testid="site-url-api-notice">
23+
This page captures the server-side URL resolution for automated testing only.
24+
</p>
25+
<pre id="site-url-api-json">{formattedSnapshot}</pre>
26+
</main>
27+
<script type="module" set:html={scriptContent}></script>
28+
</body>
29+
</html>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
import { getSiteUrl } from '@components/scripts/utils/siteUrlClient'
3+
4+
const snapshot = {
5+
siteUrl: getSiteUrl(),
6+
}
7+
8+
const formattedSnapshot = JSON.stringify(snapshot, null, 2)
9+
const scriptContent = `window.siteUrlClientSnapshot = ${JSON.stringify(snapshot).replace(/</g, '\\u003c')}`
10+
---
11+
12+
<!DOCTYPE html>
13+
<html lang="en">
14+
<head>
15+
<meta charset="utf-8">
16+
<title>Site URL Client Diagnostics</title>
17+
<meta name="robots" content="noindex, nofollow">
18+
</head>
19+
<body>
20+
<main>
21+
<h1>Site URL Client Diagnostics</h1>
22+
<p data-testid="site-url-client-notice">
23+
This page captures the client-side URL resolution for automated testing only.
24+
</p>
25+
<pre id="site-url-client-json">{formattedSnapshot}</pre>
26+
</main>
27+
<script type="module" set:html={scriptContent}></script>
28+
</body>
29+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expect } from '@test/e2e/helpers'
2+
import { BasePage } from '@test/e2e/helpers/pageObjectModels/BasePage'
3+
4+
type SiteUrlSnapshot = {
5+
siteUrl: string
6+
}
7+
8+
const navigateToDiagnosticsPage = async (page: BasePage) => {
9+
await page.goto('/testing/site-url-api', { skipCookieDismiss: true })
10+
await page.waitForLoadState('networkidle')
11+
await page.waitForFunction(() => Boolean(window.siteUrlApiSnapshot))
12+
}
13+
14+
const getSnapshot = async (page: BasePage): Promise<SiteUrlSnapshot> => {
15+
await navigateToDiagnosticsPage(page)
16+
return await page.evaluate(() => {
17+
if (!window.siteUrlApiSnapshot) {
18+
throw new Error('Site URL api snapshot not initialized')
19+
}
20+
return window.siteUrlApiSnapshot
21+
})
22+
}
23+
24+
test.describe('Site URL API Diagnostics', () => {
25+
test('should resolve localhost URL for non-Vercel dev runtime', async ({ page: playwrightPage }) => {
26+
const page = await BasePage.init(playwrightPage)
27+
const snapshot = await getSnapshot(page)
28+
expect(snapshot.siteUrl).toContain('http://localhost:')
29+
})
30+
})
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expect } from '@test/e2e/helpers'
2+
import { BasePage } from '@test/e2e/helpers/pageObjectModels/BasePage'
3+
4+
type SiteUrlSnapshot = {
5+
siteUrl: string
6+
}
7+
8+
const navigateToDiagnosticsPage = async (page: BasePage) => {
9+
await page.goto('/testing/site-url-client', { skipCookieDismiss: true })
10+
await page.waitForLoadState('networkidle')
11+
await page.waitForFunction(() => Boolean(window.siteUrlClientSnapshot))
12+
}
13+
14+
const getSnapshot = async (page: BasePage): Promise<SiteUrlSnapshot> => {
15+
await navigateToDiagnosticsPage(page)
16+
return await page.evaluate(() => {
17+
if (!window.siteUrlClientSnapshot) {
18+
throw new Error('Site URL client snapshot not initialized')
19+
}
20+
return window.siteUrlClientSnapshot
21+
})
22+
}
23+
24+
test.describe('Site URL Client Diagnostics', () => {
25+
test('should resolve localhost URL in dev server', async ({ page: playwrightPage }) => {
26+
const page = await BasePage.init(playwrightPage)
27+
const snapshot = await getSnapshot(page)
28+
expect(snapshot.siteUrl).toContain('http://localhost:')
29+
})
30+
})

0 commit comments

Comments
 (0)