ASCIInator/server/__tests__/testApi.spec.js
dev e904cdb3cb feat: full GUI, test API, auto-convert, aspect-ratio sliders
- Auto-convert on image load (0ms) and flag change (400ms debounce)
- SizeControl: linked width/height sliders with aspect-ratio lock and 0.5 font correction factor
- Full flag exposure for all 4 tools (chafa, jp2a, ascii-image-converter, img2txt)
- ChafaOptions: symbols/fill dropdowns, dither controls, work/threshold/font-ratio sliders, format select, toggles
- Jp2aOptions: color-depth, RGB weight sliders, edge controls, 8 toggles
- ImgTxtOptions: dither select with valid libcaca values, gamma slider
- OutputDisplay: ansi-to-html rendering for colored chafa output
- ShellBridge: abort-previous pattern, conversion-start/end lifecycle events
- Test API (ENABLE_TEST_API=true): /test/health, /test/convert, /test/flags/:tool, /test/imagemagick
- buildArgs: space-separated args (not = format); full schemas in SCHEMAS export
- runChafa: width/height destructured and combined into --size WxH
- Port changed to 3050; Vite on 0.0.0.0 with allowedHosts for production domain
- 98 unit tests passing across 12 test files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 10:29:45 +00:00

144 lines
4.9 KiB
JavaScript
Executable File

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import Fastify from 'fastify'
import { testRoutes } from '../routes/testApi.js'
async function buildApp() {
const app = Fastify()
await app.register(testRoutes, { prefix: '/test' })
return app
}
describe('GET /test/health', () => {
it('returns ok status with binary availability map', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/health' })
expect(res.statusCode).toBe(200)
const body = res.json()
expect(body.status).toBe('ok')
expect(body.binaries).toHaveProperty('chafa')
expect(body.binaries).toHaveProperty('jp2a')
expect(body.binaries).toHaveProperty('imagemagick')
expect(typeof body.binaries.chafa.available).toBe('boolean')
expect(body.timestamp).toBeTruthy()
})
})
describe('GET /test/flags/:tool', () => {
it('returns flag list for chafa with width/height not size', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/flags/chafa' })
expect(res.statusCode).toBe(200)
const body = res.json()
expect(body.tool).toBe('chafa')
expect(Array.isArray(body.flags)).toBe(true)
const names = body.flags.map(f => f.name)
expect(names).toContain('width')
expect(names).toContain('height')
expect(names).not.toContain('size')
expect(names).toContain('colors')
expect(names).toContain('threshold')
expect(names).toContain('format')
})
it('returns flag list for jp2a', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/flags/jp2a' })
expect(res.statusCode).toBe(200)
const body = res.json()
expect(body.tool).toBe('jp2a')
const names = body.flags.map(f => f.name)
expect(names).toContain('width')
expect(names).toContain('background')
})
it('returns 404 for unknown tool', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/flags/unknown-tool' })
expect(res.statusCode).toBe(404)
})
it('includes enum options for enum-type flags', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/flags/chafa' })
const body = res.json()
const colorsFlag = body.flags.find(f => f.name === 'colors')
expect(colorsFlag.type).toBe('enum')
expect(Array.isArray(colorsFlag.options)).toBe(true)
expect(colorsFlag.options).toContain('256')
})
it('marks bool flags as boolean type', async () => {
const app = await buildApp()
const res = await app.inject({ method: 'GET', url: '/test/flags/chafa' })
const body = res.json()
const zoomFlag = body.flags.find(f => f.name === 'zoom')
expect(zoomFlag.type).toBe('boolean')
})
})
describe('POST /test/convert — URL validation', () => {
it('rejects missing imageUrl', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/convert',
headers: { 'content-type': 'application/json' },
payload: { tool: 'chafa', flags: {} },
})
expect(res.statusCode).toBe(400)
})
it('rejects missing tool', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/convert',
headers: { 'content-type': 'application/json' },
payload: { imageUrl: 'https://example.com/img.png' },
})
expect(res.statusCode).toBe(400)
})
it('rejects non-http URL schemes', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/convert',
headers: { 'content-type': 'application/json' },
payload: { imageUrl: 'file:///etc/passwd', tool: 'chafa', flags: {} },
})
expect(res.statusCode).toBe(400)
expect(res.json().error).toMatch(/http/)
})
it('rejects unknown tool', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/convert',
headers: { 'content-type': 'application/json' },
payload: { imageUrl: 'https://example.com/img.png', tool: 'evil-tool', flags: {} },
})
expect(res.statusCode).toBe(400)
})
})
describe('POST /test/imagemagick — URL validation', () => {
it('rejects missing imageUrl', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/imagemagick',
headers: { 'content-type': 'application/json' },
payload: {},
})
expect(res.statusCode).toBe(400)
})
it('rejects file:// scheme', async () => {
const app = await buildApp()
const res = await app.inject({
method: 'POST', url: '/test/imagemagick',
headers: { 'content-type': 'application/json' },
payload: { imageUrl: 'file:///tmp/img.png' },
})
expect(res.statusCode).toBe(400)
expect(res.json().error).toMatch(/http/)
})
})