![]()
โ
Comprehensive Quality Assurance for Democratic Intelligence
๐ฏ Vitest Unit ยท Cypress E2E ยท Accessibility ยท Performance ยท 14-Language Coverage
๐ Document Owner: CEO | ๐ Version: 1.0 | ๐ Review Cycle: Quarterly
๐ข Owner: Hack23 AB (Org.nr 559534-7807) | ๐ท๏ธ Classification: ๐ข Public
Comprehensive testing strategy covering unit tests (Vitest), E2E tests (Cypress), data validation, accessibility, and performance for all 9 CIA intelligence dashboards.
| Framework | Purpose | Test Count |
|---|---|---|
| Vitest | Unit & Integration Tests | 1183+ tests |
| Cypress | E2E & Integration Tests | 150+ tests |
| Playwright | Visual Regression (planned) | TBD |
| cypress-axe | Accessibility Testing (planned) | TBD |
tests/
โโโ csv-validation.test.js # CIA CSV data validation
โโโ party-dashboard.test.js # Party dashboard unit tests
โโโ risk-dashboard.test.js # Risk dashboard unit tests (existing)
โโโ coalition-dashboard.test.js # Coalition dashboard (existing)
โโโ committees-dashboard.test.js # Committee dashboard (existing)
โโโ load-cia-stats.test.js # CIA data loading (existing)
โโโ [... 27 more test files] # News, sitemap, MCP, etc.
cypress/e2e/
โโโ all-dashboards.cy.js # Comprehensive 9 dashboard tests
โโโ dashboards.cy.js # Individual dashboard tests (updated)
โโโ dashboard-page.cy.js # Dashboard page tests (updated)
โโโ politician-dashboard.cy.js # Politician-specific tests
โโโ accessibility.cy.js # WCAG 2.1 AA tests
โโโ homepage.cy.js # Homepage tests
โโโ multi-language-sanity.cy.js # 14-language validation
โโโ news-articles.cy.js # News article tests
โโโ news-page.cy.js # News page tests
โโโ sitemap.cy.js # Sitemap tests
The aggregate-then-render news pipeline (scripts/aggregate-analysis.ts โ scripts/render-articles.ts โ scripts/render-lib/) is exercised by the following dedicated test surfaces. Any change to the pipeline must keep them green.
| Surface | What it asserts | Typical harness |
|---|---|---|
| Aggregator ordering | Per-day artifacts in analysis/daily/$DATE/$SUB/ concatenate into article.md in the canonical narrative order defined by analysis/templates/ (executive-brief โ synthesis โ significance โ stakeholders โ โฆ). Duplicate H1s and admin footers are stripped; relative links are rewritten to absolute GitHub blob URLs. | Fixture-based Vitest snapshot over a minimal analysis/daily/fixture/$SUB/ tree |
| Aggregator manifest | A .manifest.json sibling to article.md lists every consumed artifact with a SHA-256 digest. The manifest is reproducible (same input โ same manifest). | Vitest hash assertion |
| Renderer sanitisation | rehype-sanitize drops <script>, <iframe>, inline event handlers, javascript: / non-image data: URIs. It does not drop allow-listed <pre class="mermaid"> blocks, <figure>/<figcaption>, <details>/<summary>, or tables. | Vitest with adversarial markdown fixtures |
| Mermaid pass-through | A ```mermaid fenced block in the markdown survives sanitisation as <pre class="mermaid">โฆ</pre> and the rendered chrome injects js/lib/mermaid-init.mjs. | Vitest parse assertion + Cypress client-render smoke |
| JSON-LD validity | Each rendered article contains a single well-formed application/ld+json NewsArticle block with populated headline, datePublished, inLanguage, author, publisher, citation[], and about[]. | Vitest parses the script tag and validates against a lightweight schema |
| Sitemap inclusion | Every news/$DATE-$SUB-$LANG.html appears in sitemap.xml (with the correct hreflang alternates) and sitemap.html. All 14 political-intelligence_*.html pages are listed too. | Vitest fixture-based assertion in generate-sitemap tests |
| Provenance footer | Every article links to the GitHub blob URL of every methodology and template file it consumed, as recorded in the manifest. | Vitest DOM assertion |
| Naming convention | Rendered files match news/$DATE-$SUB-$LANG.html (hyphens only โ never _lang underscore). | Vitest regex over news/ directory listing |
Three fixture-based tests (generate-news-indexes, two generate-rss) historically failed on pre-existing news/*.html files with malformed article:published_time values. These fixtures are regenerated by the renderer in CI; the tests now scope themselves to the new news/$DATE-$SUB-$LANG.html naming pattern so stale files do not pollute results.
File: tests/csv-validation.test.js
Validates CIA Platform CSV exports for data quality:
describe('CIA CSV Data Validation', () => {
// File existence (18 required files)
it('should have distribution_politician_risk_levels.csv');
// Encoding validation (UTF-8/ASCII, no ๏ฟฝ characters)
it('should have valid UTF-8 encoding');
// Structure validation (headers, delimiters, data rows)
it('should have proper CSV structure');
// Schema validation (required columns, data types)
it('should have required columns');
it('should have valid numeric data types');
// Column consistency (handles quoted fields)
it('should have consistent column counts');
// Data freshness (warns at 30 days, fails at 90 days)
it('CSV files should not be older than 90 days');
});
Coverage:
File: tests/party-dashboard.test.js
Tests dashboard DOM structure and configuration:
describe('Party Dashboard', () => {
// Structure tests
it('should have party dashboard container');
it('should have dashboard title and description');
// Chart tests (3 canvas charts + 1 D3 container)
it('should have partyEffectivenessChart canvas');
it('partyEffectivenessChart should have ARIA role and label');
// Accessibility tests
it('should have semantic HTML structure');
it('all charts should have proper ARIA attributes');
it('should have screen reader descriptions');
// Configuration tests
it('should support all 8 Swedish parties');
it('should define party colors for Chart.js');
// Responsive design
it('should use dashboard-grid for responsive layout');
});
Note: Dashboard JavaScript files are browser-only IIFEs (not ES6 modules), so we test DOM structure and configuration rather than importing functions.
File: tests/visualizations.test.ts
Covers all 9 public render methods of src/browser/cia/visualizations.ts โ the primary chart rendering engine that drives every CIA intelligence dashboard. The module is now included in the Vitest coverage gate.
Chart.js mock pattern. The renderer captures Chart at module load time (const Chart = (globalThis as any).Chart;). To intercept each chart construction with a per-test spy we:
globalThis.Chart with a vi.fn() that pushes { ctx, type, data, options } into a chartCalls array.vi.resetModules() and dynamically import the module so the new Chart reference is captured.async function loadRenderer(): Promise<RendererCtor> {
vi.resetModules();
const mod = await import('../src/browser/cia/visualizations.js');
return mod.CIADashboardRenderer as unknown as RendererCtor;
}
it('passes correct party labels to Chart.js', async () => {
const Renderer = await loadRenderer();
makeCanvas('party-seats-chart');
makeCanvas('party-cohesion-chart');
new Renderer({ partyPerf: fxPartyPerf() }).renderPartyPerformance();
expect(chartCalls[0].type).toBe('bar');
expect(chartCalls[0].data.labels).toEqual(['S','M','SD','C','V','KD','L','MP']);
});
Edge cases covered. Empty/missing dashboards (early return warning paths), missing globalThis.Chart (graceful no-op), and NaN / non-finite numeric fields (rendered as 'N/A' or replaced with 0).
File: cypress/e2e/all-dashboards.cy.js (150+ tests)
Tests all 9 dashboards systematically:
// Simplified example - actual implementation has specific chart IDs
const dashboards = [
// NOTE: These are simplified placeholder values for documentation purposes.
// For actual chart IDs and counts, see cypress/e2e/all-dashboards.cy.js
{ id: 'party-dashboard', charts: 3, hasD3: true, d3Container: 'coalitionAlignmentChart' },
{ id: 'election-cycle-dashboard', charts: 4, hasD3: true, d3Container: 'decision-heatmap' },
{ id: 'committee-dashboard', charts: 3, hasD3: true, d3Container: 'committeeNetwork' },
{ id: 'coalition-dashboard', charts: 3, hasD3: true, d3Container: 'coalitionNetwork' },
{ id: 'seasonal-patterns-dashboard', charts: 4, hasD3: true, d3Container: 'seasonal-heatmap' },
{ id: 'pre-election-dashboard', charts: 5, hasD3: false },
{ id: 'anomaly-detection-dashboard', charts: 4, hasD3: true, d3Container: 'severity-heatmap' },
{ id: 'ministry-dashboard', charts: 3, hasD3: true, d3Container: 'ministryRiskHeatMap' },
{ id: 'risk-dashboard', charts: 4, hasD3: true, d3Container: 'riskHeatMap' }
];
dashboards.forEach(dashboard => {
describe(dashboard.name, () => {
it('should exist and be visible');
it('should have dashboard heading');
it('should not have error messages');
it('should have data attribution');
// Chart.js validation
dashboard.charts.forEach(chartId => {
it('should have ${chartId} canvas');
it('${chartId} should have Chart.js render monitor class');
});
// D3.js validation (if applicable)
if (dashboard.hasD3) {
it('should render D3 SVG');
it('SVG should have content');
}
// Accessibility
it('should have ARIA labels on charts');
it('should have screen reader descriptions');
// Responsive design
it('should be visible on mobile (375px)');
it('should be visible on tablet (768px)');
it('should be visible on desktop (1440px)');
});
});
Zero test skips or conditionals. All tests fail immediately when features are missing:
// โ OLD (fail-slow with conditionals)
cy.get('body').then(($body) => {
const chartContainer = $body.find('#coalitionAlignmentChart');
if (chartContainer.length > 0) {
cy.get('#coalitionAlignmentChart').should('exist');
} else {
cy.log('Coalition alignment chart not found - skipping test');
}
});
// โ
NEW (fail-fast)
cy.get('#coalitionAlignmentChart').should('exist');
File: cypress/support/commands.js
Reusable commands for dashboard testing:
// Wait for Chart.js to render
Cypress.Commands.add('waitForChart', (canvasId) => {
cy.get(`#${canvasId}`).should('be.visible');
cy.get(`#${canvasId}`).should(($canvas) => {
expect($canvas[0].width).to.be.greaterThan(0);
expect($canvas[0].height).to.be.greaterThan(0);
});
});
// Wait for D3 visualization to render
Cypress.Commands.add('waitForD3', (containerId) => {
cy.get(`#${containerId} svg`).should('exist');
cy.get(`#${containerId} svg`).should(($svg) => {
expect($svg.children().length).to.be.greaterThan(0);
});
});
// Stub CIA data for testing
Cypress.Commands.add('stubCIAData', () => {
cy.intercept('GET', '**/cia-data/**/*.csv', {
statusCode: 200,
body: 'Year,Quarter,Ballot\n2024,Q1,1000\n2024,Q2,1100'
}).as('ciaData');
});
// Test responsive design
Cypress.Commands.add('testResponsive', (selector) => {
const viewports = [
{ width: 320, height: 568, name: 'Mobile' },
{ width: 768, height: 1024, name: 'Tablet' },
{ width: 1280, height: 720, name: 'Desktop' },
{ width: 1920, height: 1080, name: 'Large Desktop' }
];
viewports.forEach(viewport => {
cy.viewport(viewport.width, viewport.height);
cy.get(selector).should('be.visible');
});
});
Tool: Playwright
Planned Coverage:
Example:
test('party-dashboard should match baseline', async ({ page }) => {
await page.goto('/');
const dashboard = page.locator('#party-dashboard');
// Wait for charts to render
await page.waitForFunction(() => {
const charts = document.querySelectorAll('.chartjs-render-monitor');
return charts.length > 0;
});
// Take screenshot
await expect(dashboard).toHaveScreenshot('party-dashboard.png', {
maxDiffPixels: 100
});
});
Tool: cypress-axe
Planned Coverage:
Example:
import 'cypress-axe';
describe('Dashboard Accessibility (WCAG 2.1 AA)', () => {
beforeEach(() => {
cy.visit('/');
cy.injectAxe();
});
it('should have no accessibility violations', () => {
cy.checkA11y();
});
it('should have sufficient color contrast', () => {
cy.checkA11y(null, {
rules: {
'color-contrast': { enabled: true }
}
});
});
it('should have ARIA labels on all charts', () => {
cy.get('canvas').each($canvas => {
expect($canvas).to.have.attr('role', 'img');
expect($canvas).to.have.attr('aria-label');
});
});
});
Goals:
Example:
describe('Dashboard Performance', () => {
it('should render party chart in < 500ms', async () => {
const start = performance.now();
await renderPartyEffectivenessChart(mockData);
const end = performance.now();
expect(end - start).toBeLessThan(500);
});
it('should not leak memory after multiple renders', async () => {
const initialMemory = process.memoryUsage().heapUsed;
for (let i = 0; i < 10; i++) {
await renderAllDashboards();
destroyAllCharts();
}
global.gc();
const finalMemory = process.memoryUsage().heapUsed;
expect(finalMemory).toBeLessThan(initialMemory * 1.5);
});
});
File: .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '26'
- run: npm ci
- run: npm test
- run: npm run test:coverage
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: coverage-report
path: coverage/
e2e-tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '26'
- run: npm ci
- run: npm run build
- run: npm run cypress:run
- uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
if: failure()
with:
name: cypress-screenshots
path: cypress/screenshots/
Coverage Reports Published to:
docs/coverage/ - HTML coverage reportcoverage/lcov.info - LCOV format for badgescoverage/json - JSON format for analysisCI fails if any metric falls below the Hack23 Secure Development Policy floor (โฅ80 % lines, โฅ70 % branches). Thresholds apply to the importable unit-testable surface โ browser-only <script>-loaded modules are exercised by Cypress E2E and CLI entry points are exercised by the news workflows; both are deliberately excluded from the Vitest gate via the documented exclude list in vitest.config.js.
| Metric | Enforced floor (ISMS) | Measured 2026-04-25 |
|---|---|---|
| Statements | โฅ 80 % | 90.26 % |
| Branches | โฅ 70 % | 80.08 % |
| Functions | โฅ 70 % | 93.42 % |
| Lines | โฅ 80 % | 91.74 % |
To re-measure after adding modules, run npm run test:coverage and read the All files row at the bottom. Update both the enforced thresholds in vitest.config.js and the measured-baseline column above in the same PR.
Authority: Secure_Development_Policy.md mandates โฅ80 % line coverage and โฅ70 % branch coverage as a hard CI gate for all Hack23 production codebases.
All PRs must pass:
| Metric | Value | Target | Status |
|---|---|---|---|
| Unit Tests | 2094 | >1000 | โ |
| E2E Tests | 150+ | >100 | โ |
| CSV Validation | 159 | 100% | โ |
| Code Coverage (lines) | 91.74 % | โฅ80 % (ISMS) | โ |
| Code Coverage (branches) | 80.08 % | โฅ70 % (ISMS) | โ |
| Test Skips | 0 | 0 | โ |
| Dashboards Covered | 9/9 | 9/9 | โ |
| Languages Tested | 1/14 | 14/14 | ๐ก |
| Test Suite | Duration | Target |
|---|---|---|
| Unit Tests (Vitest) | ~63 s | <90 s |
| E2E Tests (Cypress) | TBD | <5 min |
| Visual Tests (Playwright) | TBD | <10 min |
# Run all unit tests
npm test
# Run a specific test file
npm test tests/csv-validation.test.js
# Run a focused subset by name pattern
npm test -- --grep "imf-client"
# Run tests with coverage (enforces thresholds in vitest.config.js)
npm run test:coverage
# Watch mode (interactive)
npm run test:watch
# UI mode (visual test runner)
npm run test:ui
# Run all E2E tests (headless)
npm run cypress:run
# Run specific test file
npm run cypress:run -- --spec "cypress/e2e/all-dashboards.cy.js"
# Open Cypress UI (interactive)
npm run cypress:open
# Run critical tests only
npm run cypress:run:critical
# Run with build (full integration)
npm run e2e
# Run visual tests
npm run playwright:test
# Update baselines
npm run playwright:test -- --update-snapshots
# Show report
npm run playwright:show-report
import { describe, it, expect, beforeEach } from 'vitest';
describe('New Dashboard', () => {
beforeEach(() => {
document.body.innerHTML = `
<section id="new-dashboard">
<!-- Dashboard HTML structure -->
</section>
`;
});
it('should have dashboard container', () => {
const dashboard = document.getElementById('new-dashboard');
expect(dashboard).toBeTruthy();
});
// Add more tests...
});
describe('New Dashboard E2E', () => {
beforeEach(() => {
cy.stubCIAData();
cy.visit('/');
});
it('should display new dashboard', () => {
cy.get('#new-dashboard').should('be.visible');
});
it('should render charts', () => {
cy.get('#newChart').should('exist');
cy.waitForChart('newChart');
});
// Add more tests...
});
# Run with verbose output
npm test -- --reporter=verbose
# Run single test in watch mode
npm run test:watch tests/specific.test.js
# Use debugger
npm run test:ui # Best for debugging
# Open Cypress UI for debugging
npm run cypress:open
# Run with video recording
npm run cypress:run # Videos saved to cypress/videos/
# Run with screenshots on failure
# (enabled by default, saved to cypress/screenshots/)
cy.get('#dashboard') not cy.get('div.container')| Module | Current | Target | Priority |
|---|---|---|---|
| party-dashboard.js | ~20% | >80% | High |
| risk-dashboard.js | ~60% | >80% | Medium |
| coalition-dashboard.js | ~40% | >80% | High |
| committees-dashboard.js | ~50% | >80% | Medium |
| election-cycle-dashboard.js | ~10% | >80% | High |
| ministry-dashboard.js | ~10% | >80% | High |
| seasonal-patterns-dashboard.js | ~10% | >80% | Medium |
| pre-election-dashboard.js | ~10% | >80% | Medium |
| anomaly-detection-dashboard.js | ~10% | >80% | Medium |
Last Updated: 2026-02-18
Maintained by: Hack23 AB
Version: 1.0
Effective: 2026-04-24 ยท Authoritative hub:
analysis/imf/README.mdยทanalysis/imf/agentic-integration.mdยทanalysis/imf/indicators-inventory.jsonยทanalysis/imf/data-dictionary.mdยท.github/aw/ECONOMIC_DATA_CONTRACT.md
| Test file | Purpose |
|---|---|
tests/imf-client.test.ts | Low-level Datamapper + SDMX client behaviour |
tests/imf-codes.test.ts | Dataflow / indicator code registry validation |
tests/imf-context.test.ts | High-level getImfContext({domain, country, vintage}) shape, cache, vintage handling |
tests/imf-inventory.test.ts | Schema validation of analysis/imf/indicators-inventory.json (13 assertions) |
tests/economic-context-multi-provider.test.ts | Asserts IMF queried before WB for every economic indicator class |
tests/validate-economic-context.test.ts | Validates the IMF-first contract end-to-end |
# Offline (mocked) โ fast; runs in CI
npm test -- imf
# Online smoke (live IMF API) โ opt-in only, not in default CI
IMF_LIVE_SMOKE=1 npm test -- imf-client.live
vintage_label; tests assert presenceeconomic-context-multi-provider.test.ts asserts IMF appears before WB in fetched economic dataimf-inventory.test.ts asserts every indicator in the inventory JSON has dataflow + dimensions + vintage + supersedes pointertests/banned-patterns.test.ts (where present) asserts that WB economic codes (NY.GDP.*, FP.CPI.*, SL.UEM.*, GC.DOD.TOTL.*) are never used as primary citation in article fixturestests/imf-precedence-contract.test.ts โ synthetic test feeding both IMF + WB GDP series; asserts article cites IMFtests/imf-vintage-discipline.test.ts โ asserts cache filenames carry vintage tagsCanonical rule. Every economic claim in a Riksdagsmonitor article cites an IMF dataflow first; World Bank citations are reserved for governance, environment and social residue (the classes IMF does not publish). SCB is the Swedish-specific ground truth layer. See ECONOMIC_DATA_CONTRACT.md v2.1 for the banned-phrase list and vintage discipline (>6 mo โ annotation).
Statskontoret coverage is split across focused Vitest suites:
| Test file | Coverage |
|---|---|
tests/statskontoret-client.test.ts | Download-link extraction, XLSX workbook parsing, CSV ZIP extraction, Swedish decimal handling, injected fetch client behavior. |
tests/statskontoret-fetch.test.ts | Import-safe CLI parsing, typed CLI errors, source validation, resource classification, numeric parsing primitives. |
tests/statskontoret-inventory.test.ts | Inventory metadata, dataset coverage parity with STATSKONTORET_SOURCES, provider-decision matrix, client/CLI/persistence declarations. |
Targeted validation command:
npx vitest run tests/statskontoret-client.test.ts tests/statskontoret-fetch.test.ts tests/statskontoret-inventory.test.ts
Quality expectation: no live network calls in tests; fixtures model Statskontoret workbook/ZIP assumptions and prevent workflow regressions without depending on upstream availability.
| ๐ Platforms | ๐ฆ Open-Source Projects | ๐ก๏ธ Governance & Standards |
|---|---|---|
| ๐ณ๏ธ Riksdagsmonitor โ Swedish Parliament intelligence ๐ช๐บ EU Parliament Monitor โ European coverage ๐ต๏ธ Citizen Intelligence Agency โ political-data engine ๐ Hack23 AB โ corporate site ๐ฐ Hack23 Blog โ engineering & policy ๐ผ Hack23 on LinkedIn | ๐ณ๏ธ Hack23/riksdagsmonitor ๐ต๏ธ Hack23/cia ๐ช๐บ Hack23/euparliamentmonitor ๐ Hack23/european-parliament-mcp โ Hack23/cia-compliance-manager ๐ฅ Hack23/black-trigram ๐ Hack23/homepage | ๐ก๏ธ Hack23 ISMS-PUBLIC โ public ISMS ๐ Information Security Policy ๐ค AI Policy ๐งช Secure Development Policy ๐ฏ Threat Modeling Policy โ ๏ธ Vulnerability Management ๐ท๏ธ Classification Framework |
๐ณ๏ธ Empower citizens ยท ๐ Strengthen democratic accountability ยท ๐ต๏ธ Illuminate the political process
ยฉ 2008โ2026 Hack23 AB (Org.nr 559534-7807) ยท Maintainer: James Pether Sรถrling, CISSP CISM