- nginx: rewrite the refresh cookie's Path=/auth to /api/auth on the production proxy — without this the browser never sent the cookie back on POST /api/auth/refresh and the session was lost on reload - i18n: fallbackLng ru -> en, so only an actual ru browser locale defaults to Russian, everything else defaults to English - index.html: explicit English og:*/twitter:* tags so link previews in Discord/etc. don't fall back to the (Russian) meta description - Avatar upload: pick -> circular crop/reposition/zoom preview -> confirm -> upload, instead of uploading the raw file blind - Topbar: logo pill background removed, pixel-heart mark added next to the LoVisual wordmark - Fixed two tests left stale by the earlier tagline copy change Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
54 lines
2.1 KiB
TypeScript
54 lines
2.1 KiB
TypeScript
import { expect, test } from 'vitest'
|
|
import { clampPan, clampZoom, coverScale, sourceRect } from '../components/cropMath'
|
|
|
|
test('coverScale scales the shorter side to fill the viewport', () => {
|
|
expect(coverScale(1000, 2000, 200)).toBeCloseTo(0.2)
|
|
expect(coverScale(2000, 1000, 200)).toBeCloseTo(0.2)
|
|
})
|
|
|
|
test('clampZoom keeps zoom within [1, 4]', () => {
|
|
expect(clampZoom(0.5)).toBe(1)
|
|
expect(clampZoom(10)).toBe(4)
|
|
expect(clampZoom(2)).toBe(2)
|
|
})
|
|
|
|
test('clampPan keeps a square image centered at zoom 1 (no room to pan)', () => {
|
|
const clamped = clampPan({ x: 50, y: 50 }, 400, 400, 200, 1)
|
|
expect(clamped).toEqual({ x: 0, y: 0 })
|
|
})
|
|
|
|
test('clampPan allows panning up to the overflow at higher zoom', () => {
|
|
// 400x400 image, viewport 200, zoom 2 -> scale = 0.5*2 = 1, scaledSize 400, overflow (400-200)/2=100
|
|
const clamped = clampPan({ x: 1000, y: -1000 }, 400, 400, 200, 2)
|
|
expect(clamped).toEqual({ x: 100, y: -100 })
|
|
})
|
|
|
|
test('clampPan never lets the image show empty space (overflow stays >= 0)', () => {
|
|
const clamped = clampPan({ x: 5, y: 5 }, 300, 900, 240, 1)
|
|
// shorter side (300) covers the viewport exactly at zoom 1, so no pan room on x
|
|
expect(clamped.x).toBe(0)
|
|
// long side has plenty of room
|
|
expect(Math.abs(clamped.y)).toBeLessThanOrEqual(5)
|
|
})
|
|
|
|
test('sourceRect at zoom 1 with no offset centers the full covering square', () => {
|
|
const rect = sourceRect(400, 400, 200, 1, { x: 0, y: 0 })
|
|
expect(rect.sx).toBeCloseTo(0)
|
|
expect(rect.sy).toBeCloseTo(0)
|
|
expect(rect.sw).toBeCloseTo(400)
|
|
expect(rect.sh).toBeCloseTo(400)
|
|
})
|
|
|
|
test('sourceRect shrinks the source square as zoom increases', () => {
|
|
const rect = sourceRect(400, 400, 200, 2, { x: 0, y: 0 })
|
|
expect(rect.sw).toBeCloseTo(200)
|
|
expect(rect.sh).toBeCloseTo(200)
|
|
expect(rect.sx).toBeCloseTo(100)
|
|
expect(rect.sy).toBeCloseTo(100)
|
|
})
|
|
|
|
test('sourceRect shifts opposite the pan offset', () => {
|
|
const zoomed = sourceRect(400, 400, 200, 2, { x: 50, y: 0 })
|
|
// positive offset.x pans the image right on screen -> source window moves left
|
|
expect(zoomed.sx).toBeLessThan(100)
|
|
})
|