feat(cookie): Findings bearbeitbar — gruppiert nach Typ + Matrix + Hinweise-Split
CookieFindings: Umschalter [Nach Fehlertyp] (je Typ: Maßnahme + betroffene Cookies + Ticket-Text) ↔ [Matrix] (Cookie×Typ, ✗ Handlung / ⚠ Hinweis). Trennung FINDINGS (zu beheben) vs HINWEISE (neutral, gegen DSE zu prüfen). Backend: kind-Klassifikation (third_country/eu_alternative=hinweis); Drittland- Remediation neutral formuliert (pro Verarbeiter prüfen, keine 'in DSE benennen'- Befehle, da DSE-Abdeckung wie BMWs 'in der Regel SCC' oft unzureichend). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* CookieFindings — bereitet die Library-Befunde bearbeitbar auf, statt als
|
||||
* Fließtext-Liste. Zwei Sichten (Umschalter):
|
||||
* - Nach Fehlertyp: je Typ eine Maßnahme + betroffene Cookies + Ticket-Text
|
||||
* (= eine Ticket-Einheit). Getrennt in FINDINGS (zu beheben) und HINWEISE
|
||||
* (neutral, gegen DSE zu prüfen: Drittland, EU-Alternative).
|
||||
* - Matrix: Zeilen = Cookies, Spalten = Fehlertypen, Markierung wo nachzubessern
|
||||
* ist (ein Cookie, alle Probleme auf einen Blick).
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
|
||||
import type { CookieFinding } from './CookieLibraryPanel'
|
||||
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
tracker_as_necessary: 'Tracker als „notwendig" deklariert',
|
||||
missing_purpose: 'Zweck fehlt',
|
||||
excessive_lifetime: 'Speicherdauer zu lang',
|
||||
vague_duration: 'Speicherdauer nicht konkret',
|
||||
missing_retention: 'Keine Speicherdauer/Löschfrist',
|
||||
storage_transparency: 'Speichertyp nicht transparent',
|
||||
third_country: 'Drittland-Transfer',
|
||||
eu_alternative: 'EU-Alternative verfügbar',
|
||||
}
|
||||
const TYPE_MEASURE: Record<string, string> = {
|
||||
tracker_as_necessary: 'Als einwilligungspflichtig einstufen (§ 25 Abs. 1 TDDDG).',
|
||||
missing_purpose: 'Zweck je Cookie ergänzen (Art. 13 DSGVO).',
|
||||
vague_duration: 'Konkrete Speicherdauer oder Löschkriterium angeben (Art. 5 Abs. 1 lit. e).',
|
||||
missing_retention: 'Speicherdauer/Löschfrist je Verarbeiter festlegen (Art. 5 Abs. 1 lit. e).',
|
||||
excessive_lifetime: 'Speicherdauer auf das Erforderliche reduzieren (Art. 5 Abs. 1 lit. e).',
|
||||
storage_transparency: 'Speichertyp + -dauer je Objekt transparent ausweisen (§ 25 TDDDG).',
|
||||
third_country: 'Geeignete Garantien je Verarbeiter prüfen (SCC Art. 46 / Art. 49).',
|
||||
eu_alternative: 'EU-Alternative prüfen (kommerziell, kein Drittland-Transfer).',
|
||||
}
|
||||
const TYPE_ORDER = [
|
||||
'tracker_as_necessary', 'missing_purpose', 'vague_duration', 'missing_retention',
|
||||
'excessive_lifetime', 'storage_transparency', 'third_country', 'eu_alternative',
|
||||
]
|
||||
const SEV_ORDER: Record<string, number> = { HIGH: 0, MEDIUM: 1, LOW: 2 }
|
||||
const SEV_COLOR: Record<string, string> = {
|
||||
HIGH: 'bg-red-100 text-red-700',
|
||||
MEDIUM: 'bg-amber-100 text-amber-700',
|
||||
LOW: 'bg-blue-100 text-blue-700',
|
||||
}
|
||||
|
||||
interface Group { type: string; items: CookieFinding[]; severity: string }
|
||||
|
||||
function groupByType(findings: CookieFinding[]): Group[] {
|
||||
const m = new Map<string, CookieFinding[]>()
|
||||
for (const f of findings) {
|
||||
if (!m.has(f.type)) m.set(f.type, [])
|
||||
m.get(f.type)!.push(f)
|
||||
}
|
||||
const groups = [...m.entries()].map(([type, items]) => ({
|
||||
type, items,
|
||||
severity: items.reduce(
|
||||
(s, f) => (SEV_ORDER[f.severity] ?? 3) < (SEV_ORDER[s] ?? 3) ? f.severity : s, 'LOW'),
|
||||
}))
|
||||
groups.sort((a, b) =>
|
||||
(TYPE_ORDER.indexOf(a.type) + 99) % 100 - (TYPE_ORDER.indexOf(b.type) + 99) % 100)
|
||||
return groups
|
||||
}
|
||||
|
||||
function cookieLabel(f: CookieFinding): string {
|
||||
const v = f.vendor && f.vendor !== '—' ? ` (${f.vendor})` : ''
|
||||
const d = f.declared ? ` — ${f.declared}` : ''
|
||||
return `${f.cookie}${v}${d}`
|
||||
}
|
||||
|
||||
function ticketText(g: Group): string {
|
||||
return [
|
||||
`${TYPE_LABEL[g.type] || g.type} — ${g.items.length} betroffen`,
|
||||
`Maßnahme: ${TYPE_MEASURE[g.type] || ''}`,
|
||||
'',
|
||||
...g.items.map(f => `- ${cookieLabel(f)}`),
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function GroupCard({ g }: { g: Group }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const copy = () => {
|
||||
navigator.clipboard?.writeText(ticketText(g)).then(() => {
|
||||
setCopied(true); setTimeout(() => setCopied(false), 1500)
|
||||
}).catch(() => {})
|
||||
}
|
||||
return (
|
||||
<div className="border-b last:border-b-0">
|
||||
<button onClick={() => setOpen(o => !o)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-gray-50 text-xs">
|
||||
<span className={`text-gray-400 transition-transform ${open ? 'rotate-90' : ''}`}>›</span>
|
||||
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${SEV_COLOR[g.severity] || 'bg-gray-100'}`}>
|
||||
{g.severity}
|
||||
</span>
|
||||
<span className="font-medium text-gray-800 flex-1 min-w-0 truncate">
|
||||
{TYPE_LABEL[g.type] || g.type}
|
||||
</span>
|
||||
<span className="text-gray-500">{g.items.length}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-4 pb-3 space-y-2">
|
||||
<div className="text-xs text-gray-700 bg-blue-50 rounded px-2 py-1.5">
|
||||
<span className="font-semibold">Maßnahme:</span> {TYPE_MEASURE[g.type] || '—'}
|
||||
</div>
|
||||
<table className="w-full text-[11px]">
|
||||
<tbody>
|
||||
{g.items.map((f, i) => (
|
||||
<tr key={i} className="border-t border-gray-100 align-top">
|
||||
<td className="px-2 py-1 font-mono text-gray-700 break-all w-40">{f.cookie}</td>
|
||||
<td className="px-2 py-1 text-gray-400 w-32 truncate">{f.vendor}</td>
|
||||
<td className="px-2 py-1 text-gray-500">{f.declared || ''}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button onClick={copy}
|
||||
className="text-[11px] px-2 py-1 rounded bg-gray-100 text-gray-700 hover:bg-gray-200">
|
||||
{copied ? '✓ Ticket-Text kopiert' : 'Ticket-Text kopieren'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ title, hint, groups }: { title: string; hint?: string; groups: Group[] }) {
|
||||
if (!groups.length) return null
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="px-3 py-2 bg-slate-50 border-b">
|
||||
<span className="text-xs font-semibold text-gray-700">{title}</span>
|
||||
{hint && <span className="text-[10px] text-gray-400 ml-2">{hint}</span>}
|
||||
</div>
|
||||
{groups.map(g => <GroupCard key={g.type} g={g} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Matrix({ findings }: { findings: CookieFinding[] }) {
|
||||
const { rows, cols } = useMemo(() => {
|
||||
const colSet = new Set(findings.map(f => f.type))
|
||||
const cols = TYPE_ORDER.filter(t => colSet.has(t))
|
||||
const rowMap = new Map<string, { label: string; vendor: string; hits: Record<string, string> }>()
|
||||
for (const f of findings) {
|
||||
const key = `${f.cookie}@@${f.vendor}`
|
||||
if (!rowMap.has(key)) rowMap.set(key, { label: f.cookie, vendor: f.vendor, hits: {} })
|
||||
rowMap.get(key)!.hits[f.type] = (f.kind === 'hinweis') ? '⚠' : '✗'
|
||||
}
|
||||
return { rows: [...rowMap.values()], cols }
|
||||
}, [findings])
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-auto max-h-[32rem]">
|
||||
<table className="w-full text-[11px]">
|
||||
<thead className="bg-slate-50 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-2 py-1.5 text-left font-semibold text-gray-600">Cookie</th>
|
||||
{cols.map(c => (
|
||||
<th key={c} className="px-1 py-1.5 text-center font-normal text-gray-500" title={TYPE_LABEL[c]}>
|
||||
{(TYPE_LABEL[c] || c).split(' ')[0]}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, i) => (
|
||||
<tr key={i} className="border-t border-gray-100">
|
||||
<td className="px-2 py-1 font-mono text-gray-700 break-all">
|
||||
{r.label}
|
||||
{r.vendor && r.vendor !== '—' && <span className="text-gray-400 ml-1">· {r.vendor}</span>}
|
||||
</td>
|
||||
{cols.map(c => (
|
||||
<td key={c} className={`px-1 py-1 text-center ${r.hits[c] === '✗' ? 'text-red-600' : r.hits[c] === '⚠' ? 'text-amber-600' : 'text-gray-200'}`}>
|
||||
{r.hits[c] || '·'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="px-2 py-1.5 text-[10px] text-gray-400 border-t">
|
||||
✗ = Handlung nötig · ⚠ = Hinweis (zu prüfen) · Spalte = Fehlertyp (Tooltip)
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function CookieFindings({ findings }: { findings: CookieFinding[] }) {
|
||||
const [mode, setMode] = useState<'type' | 'matrix'>('type')
|
||||
const real = findings.filter(f => (f.kind ?? 'finding') !== 'hinweis')
|
||||
const hints = findings.filter(f => (f.kind ?? 'finding') === 'hinweis')
|
||||
|
||||
if (!findings.length) {
|
||||
return <div className="px-4 py-3 text-sm text-green-700 border rounded-lg">Keine Abweichungen gegen die Library.</div>
|
||||
}
|
||||
|
||||
const btn = (m: 'type' | 'matrix', label: string) => (
|
||||
<button onClick={() => setMode(m)}
|
||||
className={`px-2.5 py-1 rounded text-xs ${mode === m ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-semibold text-gray-800">
|
||||
{findings.length} Befund{findings.length !== 1 ? 'e' : ''}
|
||||
<span className="text-xs font-normal text-gray-400 ml-2">
|
||||
{real.length} zu beheben · {hints.length} Hinweise
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{btn('type', 'Nach Fehlertyp')}
|
||||
{btn('matrix', 'Matrix')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'matrix' ? (
|
||||
<Matrix findings={findings} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<Section title="Findings — zu beheben" groups={groupByType(real)} />
|
||||
<Section title="Hinweise — neutral, gegen DSE/Doku zu prüfen"
|
||||
hint="z.B. Drittland: interne Verträge können wir nicht einsehen"
|
||||
groups={groupByType(hints)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { CookieFindings } from './CookieFindings'
|
||||
|
||||
export interface CookieFinding {
|
||||
vendor: string
|
||||
cookie: string
|
||||
@@ -16,6 +18,7 @@ export interface CookieFinding {
|
||||
declared: string
|
||||
library_purpose: string
|
||||
remediation: string
|
||||
kind?: string
|
||||
control?: { control_id?: string | null; regulation?: string; article?: string }
|
||||
}
|
||||
|
||||
@@ -36,21 +39,6 @@ interface CheckData {
|
||||
}
|
||||
}
|
||||
|
||||
const SEV_COLOR: Record<string, string> = {
|
||||
HIGH: 'bg-red-100 text-red-700',
|
||||
MEDIUM: 'bg-amber-100 text-amber-700',
|
||||
LOW: 'bg-blue-100 text-blue-700',
|
||||
}
|
||||
const TYPE_LABEL: Record<string, string> = {
|
||||
tracker_as_necessary: 'Tracker als „notwendig" deklariert',
|
||||
missing_purpose: 'Zweck fehlt',
|
||||
excessive_lifetime: 'Speicherdauer zu lang',
|
||||
vague_duration: 'Speicherdauer nicht konkret',
|
||||
missing_retention: 'Keine Speicherdauer/Löschfrist',
|
||||
third_country: 'Drittland-Transfer',
|
||||
eu_alternative: 'EU-Alternative verfügbar',
|
||||
storage_transparency: 'Speichertyp nicht transparent',
|
||||
}
|
||||
const STORAGE_LABEL: Record<string, string> = {
|
||||
cookie: 'Cookies', local_storage: 'Local Storage',
|
||||
session_storage: 'Session Storage', indexeddb: 'IndexedDB',
|
||||
@@ -66,75 +54,45 @@ export function CookieFindingList({ data }: { data: CheckData }) {
|
||||
const driftShown =
|
||||
!!drift && ((drift.declared_count ?? 0) + (drift.browser_count ?? 0)) > 0
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{driftShown && (
|
||||
<div className="px-4 py-2.5 bg-amber-50 border-b text-xs text-amber-900">
|
||||
<span className="font-semibold">Richtlinie ↔ Realität:</span>{' '}
|
||||
<strong>{drift!.declared_count ?? 0}</strong> in der Cookie-Richtlinie
|
||||
dokumentiert · <strong>{drift!.browser_count ?? 0}</strong> im Browser geladen
|
||||
{(drift!.high_findings ?? 0) > 0 && (
|
||||
<> · <strong className="text-red-700">{drift!.high_findings} undokumentiert geladen</strong></>
|
||||
)}
|
||||
{(drift!.low_findings ?? 0) > 0 && (
|
||||
<> · {drift!.low_findings} dokumentiert, aber nicht geladen</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{inv && (inv.total ?? 0) > 0 && (
|
||||
<div className="px-4 py-2.5 bg-blue-50 border-b text-xs text-blue-900">
|
||||
<span className="font-semibold">Storage-Inventar:</span>{' '}
|
||||
{inv.total} als „Cookies" gelistet →{' '}
|
||||
<strong>{inv.real_cookies} echte Cookies</strong>
|
||||
{(inv.other_storage ?? 0) > 0 && (
|
||||
<> + <strong className="text-amber-700">{inv.other_storage} andere Endgeräte-Speicher</strong></>
|
||||
)}
|
||||
{inv.by_type && (
|
||||
<span className="text-blue-700 ml-1">
|
||||
({Object.entries(inv.by_type)
|
||||
.map(([k, n]) => `${n} ${STORAGE_LABEL[k] || k}`)
|
||||
.join(' · ')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-4 py-2.5 bg-slate-50 border-b text-sm font-semibold text-gray-800">
|
||||
Library-Abgleich — {findings.length} Befund{findings.length !== 1 ? 'e' : ''}
|
||||
<span className="ml-2 text-xs font-normal text-gray-400">
|
||||
{s.in_library ?? 0}/{s.checked ?? 0} Cookies in der Library erkannt
|
||||
</span>
|
||||
</div>
|
||||
{findings.length === 0 ? (
|
||||
<div className="px-4 py-3 text-sm text-green-700">
|
||||
Keine Abweichungen gegen die Library.
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100 max-h-96 overflow-auto">
|
||||
{findings.map((f, i) => (
|
||||
<div key={i} className="px-4 py-2.5 space-y-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${SEV_COLOR[f.severity] || 'bg-gray-100 text-gray-600'}`}>
|
||||
{f.severity}
|
||||
</span>
|
||||
<code className="text-xs text-gray-700">{f.cookie}</code>
|
||||
<span className="text-xs text-gray-400">· {f.vendor}</span>
|
||||
<span className="text-[10px] text-gray-500 ml-auto">
|
||||
{TYPE_LABEL[f.type] || f.type}
|
||||
</span>
|
||||
</div>
|
||||
{f.library_purpose && (
|
||||
<div className="text-xs text-gray-500">Library-Zweck: {f.library_purpose}</div>
|
||||
<div className="space-y-3">
|
||||
{(driftShown || (inv && (inv.total ?? 0) > 0)) && (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
{driftShown && (
|
||||
<div className="px-4 py-2.5 bg-amber-50 border-b text-xs text-amber-900">
|
||||
<span className="font-semibold">Richtlinie ↔ Realität:</span>{' '}
|
||||
<strong>{drift!.declared_count ?? 0}</strong> in der Cookie-Richtlinie
|
||||
dokumentiert · <strong>{drift!.browser_count ?? 0}</strong> im Browser geladen
|
||||
{(drift!.high_findings ?? 0) > 0 && (
|
||||
<> · <strong className="text-red-700">{drift!.high_findings} undokumentiert geladen</strong></>
|
||||
)}
|
||||
<div className="text-xs text-gray-700">{f.remediation}</div>
|
||||
{f.control?.regulation && f.control.regulation !== '—' && (
|
||||
<div className="text-[10px] text-gray-400">
|
||||
Rechtsgrundlage: {f.control.regulation} {f.control.article}
|
||||
{f.control.control_id && ` · Control ${f.control.control_id}`}
|
||||
</div>
|
||||
{(drift!.low_findings ?? 0) > 0 && (
|
||||
<> · {drift!.low_findings} dokumentiert, aber nicht geladen</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
{inv && (inv.total ?? 0) > 0 && (
|
||||
<div className="px-4 py-2.5 bg-blue-50 text-xs text-blue-900">
|
||||
<span className="font-semibold">Storage-Inventar:</span>{' '}
|
||||
{inv.total} als „Cookies" gelistet →{' '}
|
||||
<strong>{inv.real_cookies} echte Cookies</strong>
|
||||
{(inv.other_storage ?? 0) > 0 && (
|
||||
<> + <strong className="text-amber-700">{inv.other_storage} andere Endgeräte-Speicher</strong></>
|
||||
)}
|
||||
{inv.by_type && (
|
||||
<span className="text-blue-700 ml-1">
|
||||
({Object.entries(inv.by_type)
|
||||
.map(([k, n]) => `${n} ${STORAGE_LABEL[k] || k}`)
|
||||
.join(' · ')})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-[11px] text-gray-400">
|
||||
{s.in_library ?? 0}/{s.checked ?? 0} Cookies in der Library erkannt
|
||||
</div>
|
||||
<CookieFindings findings={findings} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
|
||||
import { CookieFindings } from '../CookieFindings'
|
||||
|
||||
const FINDINGS = [
|
||||
{ vendor: 'Salesforce', cookie: '_ga', type: 'tracker_as_necessary', severity: 'HIGH',
|
||||
declared: 'necessary', library_purpose: '', remediation: '', kind: 'finding' },
|
||||
{ vendor: 'Acme', cookie: 'foo', type: 'missing_purpose', severity: 'MEDIUM',
|
||||
declared: '', library_purpose: '', remediation: '', kind: 'finding' },
|
||||
{ vendor: 'Google', cookie: '_gid', type: 'third_country', severity: 'MEDIUM',
|
||||
declared: 'US', library_purpose: '', remediation: '', kind: 'hinweis' },
|
||||
]
|
||||
|
||||
describe('CookieFindings', () => {
|
||||
it('gruppiert nach Typ und trennt Findings von Hinweisen', () => {
|
||||
render(<CookieFindings findings={FINDINGS} />)
|
||||
expect(screen.getByText(/3 Befunde/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Findings — zu beheben/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Hinweise — neutral/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Tracker als/)).toBeInTheDocument()
|
||||
expect(screen.getByText('Drittland-Transfer')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('klappt eine Gruppe auf und zeigt Maßnahme + Ticket-Button', () => {
|
||||
render(<CookieFindings findings={FINDINGS} />)
|
||||
fireEvent.click(screen.getByText(/Zweck fehlt/))
|
||||
expect(screen.getByText(/Maßnahme:/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Ticket-Text kopieren/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('schaltet auf die Matrix-Sicht um', () => {
|
||||
render(<CookieFindings findings={FINDINGS} />)
|
||||
fireEvent.click(screen.getByText('Matrix'))
|
||||
expect(screen.getByText(/Handlung nötig/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('zeigt grünen Hinweis bei 0 Befunden', () => {
|
||||
render(<CookieFindings findings={[]} />)
|
||||
expect(screen.getByText(/Keine Abweichungen/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -4,21 +4,20 @@ import { render, screen } from '@testing-library/react'
|
||||
import { CookieFindingList } from '../CookieLibraryPanel'
|
||||
|
||||
describe('CookieFindingList', () => {
|
||||
it('zeigt Befunde mit Severity, Library-Zweck + Maßnahme', () => {
|
||||
it('zeigt Befunde gruppiert nach Typ mit Severity + Library-Count', () => {
|
||||
const data = {
|
||||
summary: { checked: 10, in_library: 4, findings: 1 },
|
||||
findings: [{
|
||||
vendor: 'Salesforce', cookie: '_ga', type: 'tracker_as_necessary',
|
||||
severity: 'HIGH', declared: 'necessary',
|
||||
library_purpose: 'Besucher eindeutig unterscheiden',
|
||||
remediation: 'Als einwilligungspflichtig (§ 25 TDDDG) einstufen.',
|
||||
remediation: 'Als einwilligungspflichtig (§ 25 TDDDG) einstufen.', kind: 'finding',
|
||||
}],
|
||||
}
|
||||
render(<CookieFindingList data={data} />)
|
||||
expect(screen.getByText(/1 Befund/)).toBeInTheDocument()
|
||||
expect(screen.getByText('_ga')).toBeInTheDocument()
|
||||
expect(screen.getByText('HIGH')).toBeInTheDocument()
|
||||
expect(screen.getByText(/§ 25 TDDDG/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Tracker als/)).toBeInTheDocument() // Gruppen-Header
|
||||
expect(screen.getByText(/4\/10 Cookies/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user