Skip to content

Commit dffd8d2

Browse files
committed
Implement store persistence e2e tests
1 parent 0d7b552 commit dffd8d2

4 files changed

Lines changed: 74 additions & 40 deletions

File tree

@types/window.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ declare global {
6969
* Snapshot of server-side site URL helper
7070
*/
7171
siteUrlApiSnapshot?: SiteUrlSnapshot
72+
73+
/**
74+
* Exposed store helpers that tests use to seed state when Playwright controls the browser.
75+
* These are only defined when window.isPlaywrightControlled === true.
76+
*/
77+
updateConsent?: (category: 'analytics' | 'marketing' | 'functional', value: boolean) => void
78+
cacheEmbed?: (key: string, data: unknown, ttl: number) => void
79+
saveMastodonInstance?: (domain: string) => void
7280
}
7381
}
7482

src/components/scripts/bootstrap/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
initAnimationLifecycle,
1010
initConsentFromCookies,
1111
initConsentSideEffects,
12+
exposeStoreActionsForTesting,
1213
} from '@components/scripts/store'
1314
import { SentryBootstrap } from '@components/scripts/sentry/client'
1415
import {
@@ -60,6 +61,9 @@ export class AppBootstrap {
6061
// 4. Setup all module-specific side effects (runs once per page load)
6162
addScriptBreadcrumb({ scriptName: 'AppBootstrap', operation: 'initConsentSideEffects' })
6263
initConsentSideEffects()
64+
65+
// 5. Expose limited store actions for Playwright-driven E2E tests
66+
exposeStoreActionsForTesting()
6367
} catch (error: unknown) {
6468
const scriptError = new ClientScriptError(error)
6569
throw scriptError

src/components/scripts/store/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
* Central State Management - Barrel Export
33
* Single source of truth for all client-side state
44
*/
5+
import { updateConsent } from './consent'
6+
import { cacheEmbed } from './socialEmbeds'
7+
import { saveMastodonInstance } from './mastodonInstances'
58

69
// Re-export types
710
export type {
@@ -99,3 +102,19 @@ export {
99102
type EmbedCacheEntry,
100103
type EmbedCacheState,
101104
} from './socialEmbeds'
105+
106+
/**
107+
* Expose a limited set of store actions during Playwright runs so E2E tests
108+
* can seed state without relying on private internals or DOM-only flows.
109+
*/
110+
export function exposeStoreActionsForTesting(): void {
111+
if (typeof window === 'undefined' || window.isPlaywrightControlled !== true) {
112+
return
113+
}
114+
115+
Object.assign(window, {
116+
updateConsent,
117+
cacheEmbed,
118+
saveMastodonInstance,
119+
})
120+
}

test/e2e/specs/04-components/store-persistence.spec.ts

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,8 @@ test.describe('Nanostore Persistence Across Navigation', () => {
7878
await disableConsentModal(page)
7979
})
8080

81-
test.skip('@ready theme preference persists across View Transitions', async ({ page: playwrightPage }) => {
81+
test('@ready theme preference persists across View Transitions', async ({ page: playwrightPage }) => {
8282
const page = await BasePage.init(playwrightPage)
83-
// SKIPPED: Theme persists to localStorage but isn't re-applied from localStorage after View Transition
84-
// Root cause: Theme initialization on new page doesn't read from $theme store properly
85-
// localStorage.getItem('theme') returns 'dark' but <html data-theme> is null after navigation
86-
// This is an implementation bug in theme initialization logic
87-
8883
// Go to homepage
8984
await gotoWithoutGrantingConsent(page)
9085
await page.waitForLoadState('networkidle')
@@ -140,22 +135,22 @@ test.describe('Nanostore Persistence Across Navigation', () => {
140135
expect(isOpen).toBe('true')
141136
})
142137

143-
test.skip('@ready cookie consent preferences persist across View Transitions', async ({ page: playwrightPage }) => {
138+
test('@ready cookie consent preferences persist across View Transitions', async ({ page: playwrightPage }) => {
144139
const page = await BasePage.init(playwrightPage)
145-
// SKIPPED: This test needs rework for hybrid consent approach
146-
// where functional is true by default and analytics is opt-in
147140
// Go to homepage
148141
await gotoWithoutGrantingConsent(page)
149142
await page.waitForLoadState('networkidle')
150143

151144
// Accept functional consent
152-
await page.evaluate(() => {
153-
const { updateConsent } = window as any
154-
if (updateConsent) {
155-
updateConsent('functional', true)
156-
updateConsent('analytics', true)
145+
const consentHelpersAvailable = await page.evaluate(() => {
146+
if (!window.updateConsent) {
147+
return false
157148
}
149+
window.updateConsent('functional', true)
150+
window.updateConsent('analytics', true)
151+
return true
158152
})
153+
expect(consentHelpersAvailable).toBe(true)
159154
await expect.poll(async () => {
160155
return await getLocalStorageItem(page, 'cookieConsent')
161156
}).not.toBeNull()
@@ -186,32 +181,35 @@ test.describe('Nanostore Persistence Across Navigation', () => {
186181
}
187182
})
188183

189-
test.skip('@ready social embed cache persists across View Transitions', async ({ page: playwrightPage }) => {
184+
test('@ready social embed cache persists across View Transitions', async ({ page: playwrightPage }) => {
190185
const page = await BasePage.init(playwrightPage)
191-
// SKIPPED: Test setup issue - cache functions not available in test environment
192186
// Go to a page with social embeds (if available)
193187
await gotoWithoutGrantingConsent(page)
194188
await page.waitForLoadState('networkidle')
195189

196190
// First enable functional consent (required for caching)
197-
await page.evaluate(() => {
198-
const { updateConsent } = window as any
199-
if (updateConsent) {
200-
updateConsent('functional', true)
191+
const functionalConsentApplied = await page.evaluate(() => {
192+
if (!window.updateConsent) {
193+
return false
201194
}
195+
window.updateConsent('functional', true)
196+
return true
202197
})
198+
expect(functionalConsentApplied).toBe(true)
203199
await expect.poll(async () => {
204200
return await getLocalStorageItem(page, 'cookieConsent')
205201
}).not.toBeNull()
206202

207203
// Add a mock embed to the cache
208-
await page.evaluate(() => {
209-
const { cacheEmbed } = window as any
210-
if (cacheEmbed) {
211-
const testData = { html: '<blockquote>Test cached embed</blockquote>' }
212-
cacheEmbed('test_embed_123', testData, 24 * 60 * 60 * 1000) // 24 hours
204+
const cacheHelpersAvailable = await page.evaluate(() => {
205+
if (!window.cacheEmbed) {
206+
return false
213207
}
208+
const testData = { html: '<blockquote>Test cached embed</blockquote>' }
209+
window.cacheEmbed('test_embed_123', testData, 24 * 60 * 60 * 1000) // 24 hours
210+
return true
214211
})
212+
expect(cacheHelpersAvailable).toBe(true)
215213
await expect.poll(async () => {
216214
return await getLocalStorageItem(page, 'socialEmbedCache')
217215
}).not.toBeNull()
@@ -242,32 +240,35 @@ test.describe('Nanostore Persistence Across Navigation', () => {
242240
}
243241
})
244242

245-
test.skip('@ready mastodon instances persist across View Transitions', async ({ page: playwrightPage }) => {
243+
test('@ready mastodon instances persist across View Transitions', async ({ page: playwrightPage }) => {
246244
const page = await BasePage.init(playwrightPage)
247-
// SKIPPED: Test setup issue - mastodon functions not available in test environment
248245
// Go to homepage
249246
await gotoWithoutGrantingConsent(page)
250247
await page.waitForLoadState('networkidle')
251248

252249
// First enable functional consent (required for persistence)
253-
await page.evaluate(() => {
254-
const { updateConsent } = window as any
255-
if (updateConsent) {
256-
updateConsent('functional', true)
250+
const functionalConsentEnabled = await page.evaluate(() => {
251+
if (!window.updateConsent) {
252+
return false
257253
}
254+
window.updateConsent('functional', true)
255+
return true
258256
})
257+
expect(functionalConsentEnabled).toBe(true)
259258
await expect.poll(async () => {
260259
return await getLocalStorageItem(page, 'cookieConsent')
261260
}).not.toBeNull()
262261

263262
// Add mastodon instances
264-
await page.evaluate(() => {
265-
const { saveMastodonInstance } = window as any
266-
if (saveMastodonInstance) {
267-
saveMastodonInstance('mastodon.social')
268-
saveMastodonInstance('fosstodon.org')
263+
const mastodonHelpersAvailable = await page.evaluate(() => {
264+
if (!window.saveMastodonInstance) {
265+
return false
269266
}
267+
window.saveMastodonInstance('mastodon.social')
268+
window.saveMastodonInstance('fosstodon.org')
269+
return true
270270
})
271+
expect(mastodonHelpersAvailable).toBe(true)
271272
await expect.poll(async () => {
272273
return await getLocalStorageItem(page, 'mastodonInstances')
273274
}).not.toBeNull()
@@ -313,12 +314,14 @@ test.describe('Nanostore Persistence Across Navigation', () => {
313314
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
314315

315316
// Set consent
316-
await page.evaluate(() => {
317-
const { updateConsent } = window as any
318-
if (updateConsent) {
319-
updateConsent('functional', true)
317+
const consentSet = await page.evaluate(() => {
318+
if (!window.updateConsent) {
319+
return false
320320
}
321+
window.updateConsent('functional', true)
322+
return true
321323
})
324+
expect(consentSet).toBe(true)
322325
await expect.poll(async () => {
323326
return await getLocalStorageItem(page, 'cookieConsent')
324327
}).not.toBeNull()

0 commit comments

Comments
 (0)