Added to web/package.json devDeps: eslint (9, flat config) + eslint-plugin-svelte + typescript-eslint + globals; prettier + prettier-plugin-svelte; vitest (jsdom env) + jsdom. New scripts: lint, lint:fix, format, format:check, test, test:watch. Configs: - web/eslint.config.js — flat config, TS + Svelte, browser/node globals, no-explicit-any as warn, unused-vars as error (ignores _-prefixed). - web/.prettierrc.json — single-quote, 100 width, svelte parser override. - web/.prettierignore — dist/node_modules/build/lockfiles. - web/vite.config.ts — vitest test block via reference directive, jsdom env, globals enabled. Sample test: web/src/lib/utils.test.ts (6 tests covering relativeTime, truncateMiddle, debounce — all passing). CI: new web job in .gitea/workflows/ci.yml (npm ci, check [advisory], lint [advisory], format:check [advisory], test [gate], build [gate]). Advisory steps use continue-on-error until the baseline is clean — matching the existing golangci-lint advisory pattern. Known baseline surfaced by the new toolchain (pre-existing, not caused by R8): svelte-check 154 errors (133-file config cascade), eslint 126 errors + 12 warnings (unused vars, @html XSS, unused CSS), prettier 175 unformatted files. Fixing these is a follow-up cleanup. VERSION 0.7.9 -> 0.7.10. Plan R8 marked done; C.1 updated.
55 lines
1.9 KiB
TypeScript
55 lines
1.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { relativeTime, truncateMiddle, debounce } from './utils'
|
|
|
|
describe('relativeTime', () => {
|
|
it('returns "never" for null/undefined/empty', () => {
|
|
expect(relativeTime(null)).toBe('never')
|
|
expect(relativeTime(undefined)).toBe('never')
|
|
expect(relativeTime('')).toBe('never')
|
|
})
|
|
|
|
it('returns "just now" for future timestamps', () => {
|
|
const future = new Date(Date.now() + 10_000).toISOString()
|
|
expect(relativeTime(future)).toBe('just now')
|
|
})
|
|
|
|
it('formats seconds/minutes/hours/days', () => {
|
|
const now = Date.now()
|
|
expect(relativeTime(new Date(now - 5_000).toISOString())).toBe('5s ago')
|
|
expect(relativeTime(new Date(now - 120_000).toISOString())).toBe('2m ago')
|
|
expect(relativeTime(new Date(now - 3_600_000).toISOString())).toBe('1h ago')
|
|
expect(relativeTime(new Date(now - 86_400_000 * 2).toISOString())).toBe('2d ago')
|
|
})
|
|
})
|
|
|
|
describe('truncateMiddle', () => {
|
|
it('returns the string unchanged when at or under maxLen', () => {
|
|
expect(truncateMiddle('short', 36)).toBe('short')
|
|
expect(truncateMiddle('exactly36chars_exactly36chars_xxxxx', 36)).toBe(
|
|
'exactly36chars_exactly36chars_xxxxx'
|
|
)
|
|
})
|
|
|
|
it('truncates in the middle, preserving both ends', () => {
|
|
const out = truncateMiddle('investigation:nomos/dragonfly-memlock-overrun', 20)
|
|
expect(out).toContain('…')
|
|
// keep = floor((20 - 1) / 2) = 9 chars from each end
|
|
expect(out.startsWith('investiga')).toBe(true)
|
|
expect(out.endsWith('-overrun')).toBe(true)
|
|
expect(out.length).toBeLessThanOrEqual(20)
|
|
})
|
|
})
|
|
|
|
describe('debounce', () => {
|
|
it('collapses rapid calls into one trailing invocation', async () => {
|
|
let calls = 0
|
|
const fn = debounce(() => calls++, 20)
|
|
fn()
|
|
fn()
|
|
fn()
|
|
expect(calls).toBe(0)
|
|
await new Promise((r) => setTimeout(r, 40))
|
|
expect(calls).toBe(1)
|
|
})
|
|
})
|