test: add browser-based unit tests for helpers, i18n, router
This commit is contained in:
11
AGENTS.md
11
AGENTS.md
@@ -29,6 +29,9 @@ python3 -m http.server 8080
|
||||
# Oder mit Live-Reload
|
||||
npx live-server
|
||||
|
||||
# Tests ausführen (im Browser)
|
||||
# Server starten, dann http://localhost:8080/tests/ öffnen
|
||||
|
||||
# Git Push (Token in URL)
|
||||
git push origin master
|
||||
```
|
||||
@@ -54,7 +57,6 @@ js/
|
||||
│ ├── categories.js # Kategorien Service (Baum, Übersetzungen)
|
||||
│ ├── locations.js # Standorte Service (Geo-Suche)
|
||||
│ ├── conversations.js # Zero-Knowledge Chat (E2E verschlüsselt)
|
||||
│ ├── chat.js # LocalStorage Chat (Legacy/Mock)
|
||||
│ ├── crypto.js # NaCl Encryption
|
||||
│ ├── currency.js # XMR/Fiat Umrechnung
|
||||
│ └── pow-captcha.js # Proof-of-Work Captcha (Challenge/Verify)
|
||||
@@ -81,6 +83,13 @@ css/
|
||||
assets/
|
||||
└── fonts/ # Self-hosted Fonts (Inter, Space Grotesk)
|
||||
|
||||
tests/
|
||||
├── index.html # Test-Runner UI (im Browser öffnen)
|
||||
├── test-runner.js # Minimaler Test-Framework
|
||||
├── helpers.test.js # Unit Tests für helpers.js
|
||||
├── i18n.test.js # Unit Tests für i18n.js
|
||||
└── router.test.js # Unit Tests für router.js
|
||||
|
||||
locales/
|
||||
├── de.json # Deutsch (Fallback)
|
||||
├── en.json
|
||||
|
||||
16
README.md
16
README.md
@@ -93,6 +93,18 @@ npx live-server
|
||||
|
||||
Öffne http://localhost:8080
|
||||
|
||||
### Tests ausführen
|
||||
|
||||
```bash
|
||||
# Server starten
|
||||
python3 -m http.server 8080
|
||||
|
||||
# Im Browser öffnen
|
||||
# http://localhost:8080/tests/
|
||||
```
|
||||
|
||||
Die Tests laufen im Browser und nutzen einen minimalen Test-Runner ohne externe Dependencies.
|
||||
|
||||
### Projektstruktur
|
||||
|
||||
```
|
||||
@@ -129,6 +141,10 @@ dgray/
|
||||
│ ├── de.json # Deutsch
|
||||
│ ├── en.json # English
|
||||
│ └── fr.json # Français
|
||||
├── tests/
|
||||
│ ├── index.html # Test-Runner UI
|
||||
│ ├── test-runner.js # Test-Framework
|
||||
│ └── *.test.js # Unit Tests
|
||||
└── assets/
|
||||
└── fonts/ # Self-hosted Fonts
|
||||
```
|
||||
|
||||
157
tests/helpers.test.js
Normal file
157
tests/helpers.test.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Tests for js/utils/helpers.js
|
||||
*/
|
||||
|
||||
import { test, describe, assertEquals, assertTrue } from './test-runner.js'
|
||||
import { escapeHTML, formatPrice, formatRelativeTime, debounce, truncate } from '../js/utils/helpers.js'
|
||||
|
||||
describe('escapeHTML', () => {
|
||||
test('escapes < and >', () => {
|
||||
assertEquals(escapeHTML('<script>'), '<script>')
|
||||
})
|
||||
|
||||
test('escapes quotes', () => {
|
||||
assertEquals(escapeHTML('"hello"'), '"hello"')
|
||||
})
|
||||
|
||||
test('escapes ampersand', () => {
|
||||
assertEquals(escapeHTML('a & b'), 'a & b')
|
||||
})
|
||||
|
||||
test('escapes single quotes', () => {
|
||||
assertEquals(escapeHTML("it's"), "it's")
|
||||
})
|
||||
|
||||
test('handles null', () => {
|
||||
assertEquals(escapeHTML(null), '')
|
||||
})
|
||||
|
||||
test('handles undefined', () => {
|
||||
assertEquals(escapeHTML(undefined), '')
|
||||
})
|
||||
|
||||
test('handles numbers by converting to string', () => {
|
||||
assertEquals(escapeHTML(123), '123')
|
||||
})
|
||||
|
||||
test('handles complex XSS attempt', () => {
|
||||
const xss = '<img src="x" onerror="alert(1)">'
|
||||
assertTrue(!escapeHTML(xss).includes('<'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatPrice', () => {
|
||||
test('formats EUR correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'EUR'), '€ 99.50')
|
||||
})
|
||||
|
||||
test('formats USD correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'USD'), '$ 99.50')
|
||||
})
|
||||
|
||||
test('formats CHF correctly', () => {
|
||||
assertEquals(formatPrice(99.5, 'CHF'), 'CHF 99.50')
|
||||
})
|
||||
|
||||
test('formats XMR with 4 decimals', () => {
|
||||
assertEquals(formatPrice(0.5, 'XMR'), '0.5000 ɱ')
|
||||
})
|
||||
|
||||
test('handles null price', () => {
|
||||
assertEquals(formatPrice(null, 'EUR'), '–')
|
||||
})
|
||||
|
||||
test('handles undefined price', () => {
|
||||
assertEquals(formatPrice(undefined, 'EUR'), '–')
|
||||
})
|
||||
|
||||
test('defaults to EUR', () => {
|
||||
assertEquals(formatPrice(10), '€ 10.00')
|
||||
})
|
||||
|
||||
test('handles unknown currency', () => {
|
||||
assertEquals(formatPrice(10, 'GBP'), 'GBP 10.00')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatRelativeTime', () => {
|
||||
test('formats seconds ago', () => {
|
||||
const date = new Date(Date.now() - 30000) // 30 seconds ago
|
||||
const result = formatRelativeTime(date, 'en')
|
||||
assertTrue(result.includes('second'))
|
||||
})
|
||||
|
||||
test('formats minutes ago', () => {
|
||||
const date = new Date(Date.now() - 5 * 60000) // 5 minutes ago
|
||||
const result = formatRelativeTime(date, 'en')
|
||||
assertTrue(result.includes('minute'))
|
||||
})
|
||||
|
||||
test('formats hours ago', () => {
|
||||
const date = new Date(Date.now() - 2 * 3600000) // 2 hours ago
|
||||
const result = formatRelativeTime(date, 'en')
|
||||
assertTrue(result.includes('hour'))
|
||||
})
|
||||
|
||||
test('formats days ago', () => {
|
||||
const date = new Date(Date.now() - 3 * 86400000) // 3 days ago
|
||||
const result = formatRelativeTime(date, 'en')
|
||||
assertTrue(result.includes('day'))
|
||||
})
|
||||
|
||||
test('accepts string date', () => {
|
||||
const date = new Date(Date.now() - 60000).toISOString()
|
||||
const result = formatRelativeTime(date, 'en')
|
||||
assertTrue(result.includes('minute'))
|
||||
})
|
||||
|
||||
test('respects locale de', () => {
|
||||
const date = new Date(Date.now() - 60000)
|
||||
const result = formatRelativeTime(date, 'de')
|
||||
assertTrue(result.includes('Minute'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('truncate', () => {
|
||||
test('truncates long strings', () => {
|
||||
assertEquals(truncate('Hello World', 6), 'Hello…')
|
||||
})
|
||||
|
||||
test('does not truncate short strings', () => {
|
||||
assertEquals(truncate('Hi', 10), 'Hi')
|
||||
})
|
||||
|
||||
test('handles exact length', () => {
|
||||
assertEquals(truncate('Hello', 5), 'Hello')
|
||||
})
|
||||
|
||||
test('handles null', () => {
|
||||
assertEquals(truncate(null, 10), null)
|
||||
})
|
||||
|
||||
test('handles empty string', () => {
|
||||
assertEquals(truncate('', 10), '')
|
||||
})
|
||||
|
||||
test('uses default maxLength of 100', () => {
|
||||
const long = 'a'.repeat(150)
|
||||
const result = truncate(long)
|
||||
assertEquals(result.length, 100)
|
||||
assertTrue(result.endsWith('…'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('debounce', () => {
|
||||
test('returns a function', () => {
|
||||
const fn = debounce(() => {})
|
||||
assertEquals(typeof fn, 'function')
|
||||
})
|
||||
|
||||
test('delays execution', (done) => {
|
||||
let called = false
|
||||
const fn = debounce(() => { called = true }, 50)
|
||||
|
||||
fn()
|
||||
assertEquals(called, false)
|
||||
})
|
||||
})
|
||||
91
tests/i18n.test.js
Normal file
91
tests/i18n.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Tests for js/i18n.js
|
||||
*/
|
||||
|
||||
import { test, describe, assertEquals, assertTrue } from './test-runner.js'
|
||||
import { i18n, t } from '../js/i18n.js'
|
||||
|
||||
describe('i18n.getNestedValue', () => {
|
||||
const obj = {
|
||||
home: {
|
||||
title: 'Welcome',
|
||||
nested: {
|
||||
deep: 'Deep Value'
|
||||
}
|
||||
},
|
||||
simple: 'Simple Value'
|
||||
}
|
||||
|
||||
test('gets top-level value', () => {
|
||||
assertEquals(i18n.getNestedValue(obj, 'simple'), 'Simple Value')
|
||||
})
|
||||
|
||||
test('gets nested value', () => {
|
||||
assertEquals(i18n.getNestedValue(obj, 'home.title'), 'Welcome')
|
||||
})
|
||||
|
||||
test('gets deeply nested value', () => {
|
||||
assertEquals(i18n.getNestedValue(obj, 'home.nested.deep'), 'Deep Value')
|
||||
})
|
||||
|
||||
test('returns undefined for missing key', () => {
|
||||
assertEquals(i18n.getNestedValue(obj, 'missing'), undefined)
|
||||
})
|
||||
|
||||
test('returns undefined for missing nested key', () => {
|
||||
assertEquals(i18n.getNestedValue(obj, 'home.missing'), undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('i18n.t (translation)', () => {
|
||||
// Note: These tests require translations to be loaded
|
||||
// They test the parameter interpolation logic
|
||||
|
||||
test('returns key if translation missing', () => {
|
||||
const result = i18n.t('this.key.does.not.exist')
|
||||
assertEquals(result, 'this.key.does.not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('i18n.getLocale', () => {
|
||||
test('returns current locale', () => {
|
||||
const locale = i18n.getLocale()
|
||||
assertTrue(['de', 'en', 'fr'].includes(locale))
|
||||
})
|
||||
})
|
||||
|
||||
describe('i18n.getSupportedLocales', () => {
|
||||
test('returns array of supported locales', () => {
|
||||
const locales = i18n.getSupportedLocales()
|
||||
assertTrue(Array.isArray(locales))
|
||||
assertTrue(locales.includes('de'))
|
||||
assertTrue(locales.includes('en'))
|
||||
assertTrue(locales.includes('fr'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('i18n.getLocaleDisplayName', () => {
|
||||
test('returns Deutsch for de', () => {
|
||||
assertEquals(i18n.getLocaleDisplayName('de'), 'Deutsch')
|
||||
})
|
||||
|
||||
test('returns English for en', () => {
|
||||
assertEquals(i18n.getLocaleDisplayName('en'), 'English')
|
||||
})
|
||||
|
||||
test('returns Français for fr', () => {
|
||||
assertEquals(i18n.getLocaleDisplayName('fr'), 'Français')
|
||||
})
|
||||
|
||||
test('returns locale code for unknown locale', () => {
|
||||
assertEquals(i18n.getLocaleDisplayName('xx'), 'xx')
|
||||
})
|
||||
})
|
||||
|
||||
describe('i18n.subscribe', () => {
|
||||
test('returns unsubscribe function', () => {
|
||||
const unsubscribe = i18n.subscribe(() => {})
|
||||
assertEquals(typeof unsubscribe, 'function')
|
||||
unsubscribe()
|
||||
})
|
||||
})
|
||||
124
tests/index.html
Normal file
124
tests/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>dgray.io - Tests</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-bg: #1a1a1a;
|
||||
--color-text: #f0f0f0;
|
||||
--color-passed: #4ade80;
|
||||
--color-failed: #f87171;
|
||||
--color-border: #333;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.test-summary {
|
||||
background: #222;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.test-summary .passed {
|
||||
color: var(--color-passed);
|
||||
}
|
||||
|
||||
.test-summary .failed {
|
||||
color: var(--color-failed);
|
||||
}
|
||||
|
||||
.test-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.test-list li {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.test-list li.passed {
|
||||
color: var(--color-passed);
|
||||
}
|
||||
|
||||
.test-list li.failed {
|
||||
color: var(--color-failed);
|
||||
}
|
||||
|
||||
.test-list li small {
|
||||
display: block;
|
||||
color: #888;
|
||||
font-family: monospace;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
#results {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="results">
|
||||
<h1>🧪 dgray.io Test Suite</h1>
|
||||
<p class="loading">Running tests...</p>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { renderResults } from './test-runner.js'
|
||||
|
||||
const container = document.getElementById('results')
|
||||
|
||||
// Run all test files
|
||||
async function runTests() {
|
||||
try {
|
||||
await import('./helpers.test.js')
|
||||
await import('./i18n.test.js')
|
||||
await import('./router.test.js')
|
||||
|
||||
// Clear loading message and render results
|
||||
container.querySelector('.loading').remove()
|
||||
renderResults(container)
|
||||
} catch (error) {
|
||||
container.innerHTML = `
|
||||
<h1>🧪 dgray.io Test Suite</h1>
|
||||
<p style="color: var(--color-failed)">
|
||||
Error loading tests: ${error.message}
|
||||
</p>
|
||||
<pre style="color: #888; font-size: 0.8rem; margin-top: 1rem;">${error.stack}</pre>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
runTests()
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
115
tests/router.test.js
Normal file
115
tests/router.test.js
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Tests for js/router.js
|
||||
*/
|
||||
|
||||
import { test, describe, assertEquals, assertDeepEquals, assertTrue } from './test-runner.js'
|
||||
import { router } from '../js/router.js'
|
||||
|
||||
describe('router.parseHash', () => {
|
||||
test('parses simple path', () => {
|
||||
// Save original hash
|
||||
const original = window.location.hash
|
||||
|
||||
window.location.hash = '/home'
|
||||
const result = router.parseHash()
|
||||
assertEquals(result.path, '/home')
|
||||
assertDeepEquals(result.params, {})
|
||||
|
||||
// Restore
|
||||
window.location.hash = original
|
||||
})
|
||||
|
||||
test('parses path with query params', () => {
|
||||
const original = window.location.hash
|
||||
|
||||
window.location.hash = '/search?q=test&page=2'
|
||||
const result = router.parseHash()
|
||||
assertEquals(result.path, '/search')
|
||||
assertEquals(result.params.q, 'test')
|
||||
assertEquals(result.params.page, '2')
|
||||
|
||||
window.location.hash = original
|
||||
})
|
||||
|
||||
test('returns / for empty hash', () => {
|
||||
const original = window.location.hash
|
||||
|
||||
window.location.hash = ''
|
||||
const result = router.parseHash()
|
||||
assertEquals(result.path, '/')
|
||||
|
||||
window.location.hash = original
|
||||
})
|
||||
})
|
||||
|
||||
describe('router.matchRoute', () => {
|
||||
// Register test routes
|
||||
router.register('/test', 'test-page')
|
||||
router.register('/user/:id', 'user-page')
|
||||
router.register('/post/:id/comment/:commentId', 'comment-page')
|
||||
|
||||
test('matches exact route', () => {
|
||||
const match = router.matchRoute('/test')
|
||||
assertEquals(match.componentTag, 'test-page')
|
||||
assertDeepEquals(match.params, {})
|
||||
})
|
||||
|
||||
test('matches route with single param', () => {
|
||||
const match = router.matchRoute('/user/123')
|
||||
assertEquals(match.componentTag, 'user-page')
|
||||
assertEquals(match.params.id, '123')
|
||||
})
|
||||
|
||||
test('matches route with multiple params', () => {
|
||||
const match = router.matchRoute('/post/456/comment/789')
|
||||
assertEquals(match.componentTag, 'comment-page')
|
||||
assertEquals(match.params.id, '456')
|
||||
assertEquals(match.params.commentId, '789')
|
||||
})
|
||||
|
||||
test('returns null for unmatched route', () => {
|
||||
const match = router.matchRoute('/unknown/path')
|
||||
assertEquals(match, null)
|
||||
})
|
||||
|
||||
test('does not match wrong segment count', () => {
|
||||
const match = router.matchRoute('/user/123/extra')
|
||||
assertEquals(match, null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('router.register', () => {
|
||||
test('returns router for chaining', () => {
|
||||
const result = router.register('/chain-test', 'chain-page')
|
||||
assertEquals(result, router)
|
||||
})
|
||||
})
|
||||
|
||||
describe('router.getCurrentRoute', () => {
|
||||
test('returns null before navigation', () => {
|
||||
// Note: May have a value if other tests triggered navigation
|
||||
const route = router.getCurrentRoute()
|
||||
assertTrue(route === null || typeof route === 'object')
|
||||
})
|
||||
})
|
||||
|
||||
describe('router.navigate', () => {
|
||||
test('sets hash', () => {
|
||||
const original = window.location.hash
|
||||
|
||||
router.navigate('/nav-test')
|
||||
assertTrue(window.location.hash.includes('nav-test'))
|
||||
|
||||
window.location.hash = original
|
||||
})
|
||||
|
||||
test('includes query params', () => {
|
||||
const original = window.location.hash
|
||||
|
||||
router.navigate('/nav-test', { foo: 'bar', num: '42' })
|
||||
assertTrue(window.location.hash.includes('foo=bar'))
|
||||
assertTrue(window.location.hash.includes('num=42'))
|
||||
|
||||
window.location.hash = original
|
||||
})
|
||||
})
|
||||
145
tests/test-runner.js
Normal file
145
tests/test-runner.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Minimal Test Runner for Browser
|
||||
* Run tests by opening tests/index.html in browser
|
||||
*/
|
||||
|
||||
const results = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
tests: []
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a test
|
||||
* @param {string} name - Test name
|
||||
* @param {Function} fn - Test function
|
||||
*/
|
||||
export function test(name, fn) {
|
||||
try {
|
||||
fn()
|
||||
results.passed++
|
||||
results.tests.push({ name, status: 'passed' })
|
||||
console.log(`✓ ${name}`)
|
||||
} catch (error) {
|
||||
results.failed++
|
||||
results.tests.push({ name, status: 'failed', error: error.message })
|
||||
console.error(`✗ ${name}`)
|
||||
console.error(` ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group tests
|
||||
* @param {string} name - Group name
|
||||
* @param {Function} fn - Function containing tests
|
||||
*/
|
||||
export function describe(name, fn) {
|
||||
console.group(name)
|
||||
fn()
|
||||
console.groupEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert equality
|
||||
* @param {*} actual
|
||||
* @param {*} expected
|
||||
* @param {string} [message]
|
||||
*/
|
||||
export function assertEquals(actual, expected, message = '') {
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
message || `Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert truthy
|
||||
* @param {*} value
|
||||
* @param {string} [message]
|
||||
*/
|
||||
export function assertTrue(value, message = '') {
|
||||
if (!value) {
|
||||
throw new Error(message || `Expected truthy value, got ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert falsy
|
||||
* @param {*} value
|
||||
* @param {string} [message]
|
||||
*/
|
||||
export function assertFalse(value, message = '') {
|
||||
if (value) {
|
||||
throw new Error(message || `Expected falsy value, got ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert throws
|
||||
* @param {Function} fn
|
||||
* @param {string} [message]
|
||||
*/
|
||||
export function assertThrows(fn, message = '') {
|
||||
try {
|
||||
fn()
|
||||
throw new Error(message || 'Expected function to throw')
|
||||
} catch (e) {
|
||||
if (e.message === (message || 'Expected function to throw')) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert deep equality for objects/arrays
|
||||
* @param {*} actual
|
||||
* @param {*} expected
|
||||
* @param {string} [message]
|
||||
*/
|
||||
export function assertDeepEquals(actual, expected, message = '') {
|
||||
const actualStr = JSON.stringify(actual)
|
||||
const expectedStr = JSON.stringify(expected)
|
||||
if (actualStr !== expectedStr) {
|
||||
throw new Error(
|
||||
message || `Expected ${expectedStr}, got ${actualStr}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test results
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getResults() {
|
||||
return { ...results }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render results to DOM
|
||||
* @param {HTMLElement} container
|
||||
*/
|
||||
export function renderResults(container) {
|
||||
const summary = document.createElement('div')
|
||||
summary.className = 'test-summary'
|
||||
summary.innerHTML = `
|
||||
<h2>Test Results</h2>
|
||||
<p class="passed">Passed: ${results.passed}</p>
|
||||
<p class="failed">Failed: ${results.failed}</p>
|
||||
`
|
||||
|
||||
const list = document.createElement('ul')
|
||||
list.className = 'test-list'
|
||||
|
||||
results.tests.forEach(t => {
|
||||
const li = document.createElement('li')
|
||||
li.className = t.status
|
||||
li.innerHTML = t.status === 'passed'
|
||||
? `✓ ${t.name}`
|
||||
: `✗ ${t.name}<br><small>${t.error}</small>`
|
||||
list.appendChild(li)
|
||||
})
|
||||
|
||||
container.appendChild(summary)
|
||||
container.appendChild(list)
|
||||
}
|
||||
Reference in New Issue
Block a user