- 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>
144 lines
4.9 KiB
JavaScript
Executable File
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/)
|
|
})
|
|
})
|