262 lines
7.7 KiB
TypeScript
262 lines
7.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
|
|
const source = async (relativePath: string) => {
|
|
return await readFile(
|
|
join(process.cwd(), ...relativePath.split("/")),
|
|
"utf8",
|
|
);
|
|
};
|
|
|
|
test("audits dashboard page uses a dedicated board component", async () => {
|
|
const dashboardPageSource = await source("app/dashboard/audits/page.tsx");
|
|
|
|
assert.doesNotMatch(
|
|
dashboardPageSource,
|
|
/DashboardPlaceholderPage/i,
|
|
"Dashboard audits route should not render the placeholder page.",
|
|
);
|
|
assert.match(
|
|
dashboardPageSource,
|
|
/<AuditsBoard \/>/,
|
|
"Audits board should be mounted from route page.",
|
|
);
|
|
assert.match(
|
|
dashboardPageSource,
|
|
/"@\/components\/audits\/audits-board"/,
|
|
"Audits board should be imported from components.",
|
|
);
|
|
});
|
|
|
|
test("audits board renders compact list with dashboard rows query and core columns", async () => {
|
|
const boardSource = await source("components/audits/audits-board.tsx");
|
|
|
|
assert.match(
|
|
boardSource,
|
|
/\"use client\"/,
|
|
"AuditsBoard must be a Client Component for useQuery.",
|
|
);
|
|
assert.match(
|
|
boardSource,
|
|
/useQuery\s*\(\s*api\.audits\.listDashboardRows,\s*\{\s*limit:\s*100\s*\}\s*\)/,
|
|
"AuditsBoard should call api.audits.listDashboardRows with { limit: 100 }.",
|
|
);
|
|
assert.match(
|
|
boardSource,
|
|
/sort\(\(\s*a,\s*b\s*\)\s*=>\s*b\.updatedAt\s*-\s*a\.updatedAt\)/,
|
|
"Dashboard rows should be sorted newest first.",
|
|
);
|
|
assert.match(boardSource, /Loading|lädt|Lade/i);
|
|
assert.match(boardSource, /Keine Audits|keine Audits/i);
|
|
assert.match(boardSource, /Slug/);
|
|
assert.match(boardSource, /Domain/);
|
|
assert.match(boardSource, /Status/);
|
|
assert.match(boardSource, /Seiten/);
|
|
assert.match(boardSource, /Generierung läuft|Generierung laeuft/);
|
|
assert.match(boardSource, /Fehlgeschlagen/);
|
|
assert.match(boardSource, /Wartet auf finales Audit/);
|
|
assert.match(boardSource, /Wartet auf Start/);
|
|
assert.match(boardSource, /Abgebrochen/);
|
|
assert.match(
|
|
boardSource,
|
|
/href=\{row\.detailHref\}/,
|
|
"Final audit rows should link through their detailHref.",
|
|
);
|
|
assert.match(
|
|
boardSource,
|
|
/row\.kind\s*===\s*"audit"/,
|
|
"Board should branch between final audit rows and generation rows.",
|
|
);
|
|
});
|
|
|
|
test("audit detail component uses getDetail query and renders skills overview section", async () => {
|
|
const detailSource = await source("components/audits/audit-detail.tsx");
|
|
|
|
assert.match(
|
|
detailSource,
|
|
/\"use client\"/,
|
|
"AuditDetail must be client-side for Convex query calls.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/api\.audits[\s\S]{0,80}getDetail/,
|
|
"AuditDetail should use api.audits.getDetail query.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/useQuery\(\s*api\.audits\.getDetail,\s*\{/,
|
|
"AuditDetail should call useQuery with api.audits.getDetail directly.",
|
|
);
|
|
assert.doesNotMatch(
|
|
detailSource,
|
|
/const\s+auditDetailQueryRef/,
|
|
"AuditDetail should not use a cast-based query fallback variable.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/const\s+audit\s*=\s*result\?\.audit;/,
|
|
"AuditDetail should destructure audit from result.audit.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/const\s+lead\s*=\s*result\?\.lead;/,
|
|
"AuditDetail should destructure lead from result.lead.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/const\s+findings\s*=/,
|
|
"AuditDetail should derive findings from result.findings.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/leadSummary\(\s*lead\s*\)/,
|
|
"AuditDetail should pass lead into leadSummary from result.lead.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/usedSkills/,
|
|
"AuditDetail should inspect usedSkills for overview rendering.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/Keine Skills gespeichert/,
|
|
"AuditDetail should show fallback text when no skills are saved.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/Verwendete Skills/,
|
|
"AuditDetail should render Verwendete Skills heading.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/Lead|lead/,
|
|
"AuditDetail should surface lead context when available.",
|
|
);
|
|
assert.doesNotMatch(
|
|
detailSource,
|
|
/<p[^>]*>\s*\{leadSummary\(\s*lead\|[\s\S]*?\)\s*\}\s*<\/p>/,
|
|
"Lead summary should not wrap leadSummary output in a nested <p>.",
|
|
);
|
|
assert.doesNotMatch(
|
|
detailSource,
|
|
/<p[^>]*>\s*\{leadSummary\(\s*audit\.lead\)\s*\}\s*<\/p>/,
|
|
"Lead summary should not wrap leadSummary output in a nested <p>.",
|
|
);
|
|
});
|
|
|
|
test("audit detail component renders verified findings before checked-page evidence", async () => {
|
|
const detailSource = await source("components/audits/audit-detail.tsx");
|
|
const findingsIndex = detailSource.indexOf("Geprüfte Befunde");
|
|
const checkedPagesIndex = detailSource.indexOf("Geprüfte Seiten");
|
|
|
|
assert.notEqual(findingsIndex, -1, "AuditDetail should render a findings section.");
|
|
assert.notEqual(checkedPagesIndex, -1, "AuditDetail should still render checked pages.");
|
|
assert.equal(
|
|
findingsIndex < checkedPagesIndex,
|
|
true,
|
|
"Findings should be rendered before raw checked-page evidence.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/findings\.map/,
|
|
"AuditDetail should render one row per verified finding.",
|
|
);
|
|
for (const field of [
|
|
"claim",
|
|
"recommendation",
|
|
"customerBenefit",
|
|
"evidenceRefs",
|
|
"confidence",
|
|
]) {
|
|
assert.match(
|
|
detailSource,
|
|
new RegExp(field),
|
|
`AuditDetail should surface finding.${field}.`,
|
|
);
|
|
}
|
|
assert.match(
|
|
detailSource,
|
|
/Evidence|Beleg|Quelle/,
|
|
"AuditDetail should label evidence chips for each finding.",
|
|
);
|
|
});
|
|
|
|
test("audit detail component renders compact checked-page evidence", async () => {
|
|
const detailSource = await source("components/audits/audit-detail.tsx");
|
|
|
|
assert.match(
|
|
detailSource,
|
|
/sourceSummaries/,
|
|
"AuditDetail should read sourceSummaries from getDetail.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/checkedPageEvidence/,
|
|
"AuditDetail should derive checked page evidence from sourceSummaries.checkedPages.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/Geprüfte Seiten/,
|
|
"AuditDetail should render a checked-pages evidence card.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/checkedPageEvidence\.map/,
|
|
"AuditDetail should render one compact row per checked page.",
|
|
);
|
|
for (const label of [
|
|
"Meta",
|
|
"Kontaktformular",
|
|
"CTA",
|
|
"Interne Links",
|
|
]) {
|
|
assert.match(
|
|
detailSource,
|
|
new RegExp(label),
|
|
`AuditDetail should expose ${label} evidence for each page.`,
|
|
);
|
|
}
|
|
assert.match(
|
|
detailSource,
|
|
/page\.screenshots\.map/,
|
|
"AuditDetail should render optional screenshot thumbnails when present.",
|
|
);
|
|
assert.match(
|
|
detailSource,
|
|
/<img[\s\S]*src=\{screenshot\.url\}/,
|
|
"AuditDetail should render screenshot URLs from the detail query.",
|
|
);
|
|
});
|
|
|
|
test("audits detail route passes id to AuditDetail via Promise params", async () => {
|
|
const pageSource = await source("app/dashboard/audits/[id]/page.tsx");
|
|
|
|
assert.match(
|
|
pageSource,
|
|
/params:\s*Promise<\{\s*id:\s*string\s*\}>/,
|
|
"Audit detail route should accept params as Promise in Next.js 16 style.",
|
|
);
|
|
assert.match(
|
|
pageSource,
|
|
/const \{\s*id\s*\}\s*=\s*await params/,
|
|
"Audit detail route should unwrap Promise params.",
|
|
);
|
|
assert.match(
|
|
pageSource,
|
|
/<AuditDetail\s+id=/,
|
|
"Audit detail route should pass id prop into AuditDetail.",
|
|
);
|
|
});
|
|
|
|
test("public audit page does not expose used skills", async () => {
|
|
const publicAuditSource = await source("app/audit/[slug]/page.tsx");
|
|
|
|
assert.doesNotMatch(
|
|
publicAuditSource,
|
|
/Verwendete Skills|usedSkills/i,
|
|
"Public audit page must not show used skills.",
|
|
);
|
|
});
|