Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 86 additions & 6 deletions app/routes/EditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,102 @@
import React from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useLoaderData } from 'react-router'
import { useTranslation } from 'react-i18next'
import { getAdvisory } from '~/auth/backend'
import { getAppConfig } from '~/auth/appConfig'
import type { Route } from './+types/EditPage'

export const clientLoader = ({ params }: Route.ClientLoaderArgs) => {
return { id: params.advisoryId }
export const clientLoader = async ({ params }: Route.ClientLoaderArgs) => {
const [detail, appConfig] = await Promise.all([
getAdvisory(params.advisoryId),
getAppConfig(),
])
if (!detail.ok) {
throw new Response(detail.error.message, { status: detail.error.status })
}
return {
advisoryId: params.advisoryId,
revision: detail.data.revision,
csaf: detail.data.csaf,
validatorUrl: appConfig.validatorUrl,
}
}

type EditorElement = HTMLElement & {
doc?: unknown
locale?: string | undefined
validatorUrl?: string | undefined
}

/** Property writes on an undefined custom element are lost, so wait for its definition. */
async function whenEditorReady(
ref: React.RefObject<EditorElement | null>,
signal: AbortSignal,
): Promise<EditorElement | undefined> {
await customElements.whenDefined('secvisogram-editor')
return signal.aborted ? undefined : (ref.current ?? undefined)
}

const EditPage: React.FC = () => {
const data = useLoaderData<typeof clientLoader>()
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const editorRef = useRef<EditorElement>(null)
const [doc, setDoc] = useState(data.csaf)
const [valid, setValid] = useState(true)

useEffect(() => {
const controller = new AbortController()
whenEditorReady(editorRef, controller.signal).then((editor) => {
if (editor) editor.locale = i18n.language
})
return () => controller.abort()
}, [i18n.language])

useEffect(() => {
const controller = new AbortController()
whenEditorReady(editorRef, controller.signal).then((editor) => {
if (!editor) return
editor.validatorUrl = data.validatorUrl
editor.doc = data.csaf
setDoc(data.csaf)
})
return () => controller.abort()
}, [data.csaf, data.validatorUrl])

useEffect(() => {
const editor = editorRef.current
if (!editor) return
const controller = new AbortController()
const { signal } = controller
editor.addEventListener(
'csaf-change',
(event) => {
setDoc((event as CustomEvent<{ doc: unknown }>).detail.doc)
},
{ signal },
)
editor.addEventListener(
'csaf-validate',
(event) => {
setValid((event as CustomEvent<{ valid: boolean }>).detail.valid)
},
{ signal },
)
return () => controller.abort()
}, [])

return (
<div className="max-w-4xl mx-auto mt-2 px-4">
<h2 className="text-lg font-bold">
{t('editPage.heading')} <code>{data.id}</code>
{t('editPage.heading')} <code>{data.advisoryId}</code>
</h2>
<secvisogram-editor locale="en"></secvisogram-editor>
<secvisogram-editor ref={editorRef}></secvisogram-editor>
{/* Test-only seam for the tracked doc/validity state; ticket 18 reads this for Save. */}
<pre data-testid="edit_page_tracked_doc" hidden>
{JSON.stringify(doc)}
</pre>
<p data-testid="edit_page_tracked_valid" hidden>
{String(valid)}
</p>
</div>
)
}
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e/AdvisoryDashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ test.beforeEach(async ({ page }) => {
await page.route('**/api/v1/advisories', (route) =>
route.fulfill({ json: advisories }),
)
await page.route('**/api/v1/advisories/advisory-1', (route) =>
route.fulfill({
json: {
...advisories[0],
revision: 'rev-1',
csaf: { document: { csaf_version: '2.1' } },
},
}),
)
})

test('renders advisory rows with the expected columns, each linking to its edit page', async ({
Expand Down
64 changes: 58 additions & 6 deletions tests/e2e/EditPage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { expect, test } from '@playwright/test'

test('passes locale="en" to the embedded secvisogram-editor element', async ({
page,
}) => {
const csaf = {
document: { csaf_version: '2.1', title: 'Example advisory' },
}

async function mockBackend(
page: import('@playwright/test').Page,
overrides?: { validatorUrl?: string },
) {
await page.route(
'**/.well-known/appspecific/de.bsi.secvisogram.json',
(route) =>
Expand All @@ -11,17 +16,64 @@ test('passes locale="en" to the embedded secvisogram-editor element', async ({
loginUrl: '/oauth2/sign_in',
logoutUrl: '/oauth2/sign_out',
userInfoUrl: '/oauth2/userinfo',
validatorUrl: overrides?.validatorUrl,
},
}),
)
await page.route('**/oauth2/userinfo', (route) =>
route.fulfill({ json: { groups: ['editor'] } }),
)
await page.route('**/api/v1/advisories/some-advisory-id', (route) =>
route.fulfill({
json: { advisoryId: 'some-advisory-id', revision: 'rev-1', csaf },
}),
)
}

test('fetches the advisory detail and passes doc/locale/validatorUrl to the element', async ({
page,
}) => {
await mockBackend(page, { validatorUrl: 'https://validator.example' })

await page.goto('/some-advisory-id/edit')

await expect(page.getByTestId('secvisogram-mock-doc')).toHaveValue(
JSON.stringify(csaf, null, 2),
)

const properties = await page.locator('secvisogram-editor').evaluate((el) => {
const editor = el as unknown as { locale?: string; validatorUrl?: string }
return { locale: editor.locale, validatorUrl: editor.validatorUrl }
})
expect(properties).toEqual({
locale: 'en',
validatorUrl: 'https://validator.example',
})
})

test('tracks the latest doc value from csaf-change events', async ({
page,
}) => {
await mockBackend(page)

await page.goto('/some-advisory-id/edit')

await expect(page.locator('secvisogram-editor')).toHaveAttribute(
'locale',
'en',
const edited = { document: { csaf_version: '2.1', title: 'Edited title' } }
await page.getByTestId('secvisogram-mock-doc').fill(JSON.stringify(edited))

await expect(page.getByTestId('edit_page_tracked_doc')).toHaveText(
JSON.stringify(edited),
)
})

test('tracks validity from csaf-validate events', async ({ page }) => {
await mockBackend(page)

await page.goto('/some-advisory-id/edit')

await expect(page.getByTestId('edit_page_tracked_valid')).toHaveText('true')

await page.getByTestId('secvisogram-mock-valid-toggle').uncheck()

await expect(page.getByTestId('edit_page_tracked_valid')).toHaveText('false')
})
9 changes: 9 additions & 0 deletions tests/e2e/SecvisogramMockElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ async function gotoEditPage(page: import('@playwright/test').Page) {
await page.route('**/oauth2/userinfo', (route) =>
route.fulfill({ json: { groups: ['editor'] } }),
)
await page.route('**/api/v1/advisories/some-advisory-id', (route) =>
route.fulfill({
json: {
advisoryId: 'some-advisory-id',
revision: 'rev-1',
csaf: null,
},
}),
)
await page.goto('/some-advisory-id/edit')
}

Expand Down
Loading