Skip to content

Commit c5afcb5

Browse files
committed
Refactor privacy my-data page to Lit web component, add E2E test
1 parent 15cdc04 commit c5afcb5

10 files changed

Lines changed: 859 additions & 436 deletions

File tree

playwright.config.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -170,55 +170,42 @@ export default defineConfig({
170170
name: 'chromium-serial',
171171
use: { ...devices['Desktop Chrome'] },
172172
testMatch: testMatchSerial,
173-
workers: 1,
174173
},
175174

176175
{
177176
name: 'firefox-serial',
178177
use: { ...devices['Desktop Firefox'] },
179-
dependencies: ['chromium-serial'],
180178
testMatch: testMatchSerial,
181-
workers: 1,
182179
},
183180

184181
{
185182
name: 'webkit-serial',
186183
use: { ...devices['Desktop Safari'] },
187-
dependencies: ['firefox-serial'],
188184
testMatch: testMatchSerial,
189-
workers: 1,
190185
},
191186

192187
/** Test against mobile viewports. */
193188
{
194189
name: 'mobile-chrome-serial',
195190
use: { ...devices['Pixel 5'] },
196-
dependencies: ['webkit-serial'],
197191
testMatch: testMatchSerial,
198-
workers: 1,
199192
},
200193
{
201194
name: 'mobile-safari-serial',
202195
use: { ...devices['iPhone 12'] },
203-
dependencies: ['mobile-chrome-serial'],
204196
testMatch: testMatchSerial,
205-
workers: 1,
206197
},
207198

208199
/** Test against branded browsers. */
209200
{
210201
name: 'microsoft-edge-serial',
211202
use: { ...devices['Desktop Edge'], channel: 'msedge' },
212-
dependencies: ['mobile-safari-serial'],
213203
testMatch: testMatchSerial,
214-
workers: 1,
215204
},
216205
{
217206
name: 'google-chrome-serial',
218207
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
219-
dependencies: ['microsoft-edge-serial'],
220208
testMatch: testMatchSerial,
221-
workers: 1,
222209
},
223210

224211
/**
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import PrivacyForm from '@components/Forms/Privacy/index.astro'
4+
import type { PrivacyFormElement as PrivacyFormElementInstance } from '../index'
5+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
6+
import { executeRender } from '@test/unit/helpers/litRuntime'
7+
import { getPrivacyFormElements } from '../selectors'
8+
9+
type PrivacyFormModule = WebComponentModule<PrivacyFormElementInstance>
10+
11+
type ActionResult<TData> = { data?: TData; error?: { message?: string } }
12+
13+
type VerifyResult =
14+
| { status: 'download'; filename: string; json: string }
15+
| { status: 'deleted' }
16+
| { status: 'expired' }
17+
18+
const requestDataMock = vi.fn<
19+
(_input: { email: string; requestType: 'ACCESS' | 'DELETE' }) => Promise<ActionResult<{ message: string }>>
20+
>()
21+
22+
const verifyDsarMock = vi.fn<(_input: { token: string }) => Promise<ActionResult<VerifyResult>>>()
23+
24+
vi.mock('astro:actions', () => ({
25+
actions: {
26+
gdpr: {
27+
requestData: requestDataMock,
28+
verifyDsar: verifyDsarMock,
29+
},
30+
},
31+
}))
32+
33+
async function flushMicrotasks(): Promise<void> {
34+
await Promise.resolve()
35+
await Promise.resolve()
36+
}
37+
38+
describe('PrivacyForm behavior', () => {
39+
let container: AstroContainer
40+
41+
beforeEach(async () => {
42+
container = await AstroContainer.create()
43+
requestDataMock.mockReset()
44+
verifyDsarMock.mockReset()
45+
})
46+
47+
it('submits an access request and shows success', async () => {
48+
requestDataMock.mockResolvedValue({ data: { message: 'Access request sent.' } })
49+
50+
await executeRender<PrivacyFormModule>({
51+
container,
52+
component: PrivacyForm,
53+
moduleSpecifier: '@components/Forms/Privacy/client/index',
54+
args: {
55+
props: {
56+
status: undefined,
57+
},
58+
},
59+
waitForReady: async (element: PrivacyFormElementInstance) => {
60+
window.history.replaceState({}, '', 'http://localhost/privacy/my-data')
61+
element.initialize()
62+
},
63+
assert: async ({ element }) => {
64+
const elements = getPrivacyFormElements(element)
65+
elements.accessEmailInput.value = 'test@example.com'
66+
67+
elements.accessForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
68+
await flushMicrotasks()
69+
70+
expect(requestDataMock).toHaveBeenCalledWith({ email: 'test@example.com', requestType: 'ACCESS' })
71+
expect(elements.accessMessage.textContent).toBe('Access request sent.')
72+
expect(elements.accessMessage.classList.contains('hidden')).toBe(false)
73+
expect(elements.accessMessage.classList.contains('border-success')).toBe(true)
74+
expect(elements.accessEmailInput.value).toBe('')
75+
},
76+
})
77+
})
78+
79+
it('blocks delete submit when confirmation is not checked', async () => {
80+
requestDataMock.mockResolvedValue({ data: { message: 'Delete request sent.' } })
81+
82+
await executeRender<PrivacyFormModule>({
83+
container,
84+
component: PrivacyForm,
85+
moduleSpecifier: '@components/Forms/Privacy/client/index',
86+
args: {
87+
props: {
88+
status: undefined,
89+
},
90+
},
91+
waitForReady: async (element: PrivacyFormElementInstance) => {
92+
window.history.replaceState({}, '', 'http://localhost/privacy/my-data')
93+
element.initialize()
94+
},
95+
assert: async ({ element }) => {
96+
const elements = getPrivacyFormElements(element)
97+
elements.deleteEmailInput.value = 'test@example.com'
98+
elements.deleteConfirmCheckbox.checked = false
99+
100+
elements.deleteForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
101+
await flushMicrotasks()
102+
103+
expect(requestDataMock).not.toHaveBeenCalled()
104+
expect(elements.deleteMessage.textContent).toBe('Please confirm you understand the deletion request.')
105+
expect(elements.deleteMessage.classList.contains('border-danger')).toBe(true)
106+
},
107+
})
108+
})
109+
110+
it('verifies token, downloads JSON, then redirects', async () => {
111+
verifyDsarMock.mockResolvedValue({
112+
data: { status: 'download', filename: 'dsar.json', json: '{"ok":true}' },
113+
})
114+
115+
const downloadJsonSpy = vi.fn()
116+
const navigateToSpy = vi.fn()
117+
118+
await executeRender<PrivacyFormModule>({
119+
container,
120+
component: PrivacyForm,
121+
moduleSpecifier: '@components/Forms/Privacy/client/index',
122+
args: {
123+
props: {
124+
status: undefined,
125+
},
126+
},
127+
waitForReady: async (element: PrivacyFormElementInstance) => {
128+
window.history.replaceState({}, '', 'http://localhost/privacy/my-data?token=unit-test-token')
129+
;(element as unknown as { downloadJson: typeof downloadJsonSpy }).downloadJson = downloadJsonSpy
130+
;(element as unknown as { navigateTo: typeof navigateToSpy }).navigateTo = navigateToSpy
131+
element.initialize()
132+
},
133+
assert: async () => {
134+
await flushMicrotasks()
135+
136+
expect(verifyDsarMock).toHaveBeenCalledWith({ token: 'unit-test-token' })
137+
expect(downloadJsonSpy).toHaveBeenCalledWith('dsar.json', '{"ok":true}')
138+
expect(navigateToSpy).toHaveBeenCalledWith('/privacy/my-data?status=already-completed')
139+
},
140+
})
141+
})
142+
})
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
import { experimental_AstroContainer as AstroContainer } from 'astro/container'
3+
import PrivacyForm from '@components/Forms/Privacy/index.astro'
4+
import type { PrivacyFormElement as PrivacyFormElementInstance } from '../index'
5+
import type { WebComponentModule } from '@components/scripts/@types/webComponentModule'
6+
import { executeRender } from '@test/unit/helpers/litRuntime'
7+
import { getPrivacyFormElements } from '../selectors'
8+
9+
type PrivacyFormModule = WebComponentModule<PrivacyFormElementInstance>
10+
11+
type ActionResult<TData> = { data?: TData; error?: { message?: string } }
12+
13+
const requestDataMock = vi.fn<
14+
(_input: { email: string; requestType: 'ACCESS' | 'DELETE' }) => Promise<ActionResult<{ message: string }>>
15+
>()
16+
17+
const verifyDsarMock = vi.fn<(_input: { token: string }) => Promise<ActionResult<{ status: string }>>>()
18+
19+
vi.mock('astro:actions', () => ({
20+
actions: {
21+
gdpr: {
22+
requestData: requestDataMock,
23+
verifyDsar: verifyDsarMock,
24+
},
25+
},
26+
}))
27+
28+
describe('PrivacyForm selectors', () => {
29+
let container: AstroContainer
30+
31+
beforeEach(async () => {
32+
container = await AstroContainer.create()
33+
requestDataMock.mockReset()
34+
verifyDsarMock.mockReset()
35+
})
36+
37+
it('stays in sync with the PrivacyForm layout', async () => {
38+
await executeRender<PrivacyFormModule>({
39+
container,
40+
component: PrivacyForm,
41+
moduleSpecifier: '@components/Forms/Privacy/client/index',
42+
args: {
43+
props: {
44+
status: undefined,
45+
},
46+
},
47+
waitForReady: async (element: PrivacyFormElementInstance) => {
48+
window.history.replaceState({}, '', 'http://localhost/privacy/my-data')
49+
element.initialize()
50+
},
51+
assert: async ({ element }) => {
52+
const elements = getPrivacyFormElements(element)
53+
54+
expect(elements.accessForm.id).toBe('access-form')
55+
expect(elements.accessEmailInput.id).toBe('access-email')
56+
expect(elements.accessMessage.id).toBe('access-message')
57+
58+
expect(elements.deleteForm.id).toBe('delete-form')
59+
expect(elements.deleteEmailInput.id).toBe('delete-email')
60+
expect(elements.deleteConfirmCheckbox.id).toBe('confirm-delete')
61+
expect(elements.deleteMessage.id).toBe('delete-message')
62+
63+
expect(elements.accessEmailInput.tagName).toBe('INPUT')
64+
expect(elements.deleteEmailInput.tagName).toBe('INPUT')
65+
},
66+
})
67+
})
68+
})

0 commit comments

Comments
 (0)