OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / test / resize-plan.test.mjs

80 lines · javascript ref:
// What to do with a picture before uploading it.
//
// ⛔ THE CAMERA IS THE CASE. Since 2026-08-18 the phone's composer can open the camera (the chooser
// offers it with no native code), and a phone camera JPEG is 3–12 MB. compose.js REFUSED anything over
// 1024 KB with an alert — so "attach a photo" was a dead end by construction, and raising the cap alone
// would not have helped: 12 MB exceeds the protocol limit too.
//
// The protocol limit is not ours to choose. app.bsky.embed.images specifies
//   "blob": { "accept": ["image/*"], "maxSize": 2000000 }
// so 2,000,000 bytes is the real ceiling, 4 images per post, alt required. Our old 1024*1024 was roughly
// half that and carried no justification in the source — unlike the 4-image cap, which cites bsky
// rejecting a longer array.
//
// Pure so it runs under node with the rest of the suite; the canvas work that USES it lives in
// compose.js, where it cannot be tested without a browser.
import { MAX_BYTES, MAX_EDGE, resizePlan, aspectRatioOf } from '../web/static/resize-plan.js'

let pass = 0
const ok = (c, m) => { if (!c) { console.error('FAIL:', m); process.exit(1) } console.log('  ok -', m); pass++ }
const eq = (a, b, m) => ok(a === b, `${m} (got ${JSON.stringify(a)}, want ${JSON.stringify(b)})`)

// The ceiling is the LEXICON's, quoted here so lowering it silently fails the suite with the reason.
eq(MAX_BYTES, 2000000, 'the ceiling is app.bsky.embed.images maxSize, not a number we picked')

// A small picture is left completely alone — re-encoding a 200 KB image would lose quality for nothing.
{
  const p = resizePlan({ width: 800, height: 600, bytes: 200000, type: 'image/jpeg' })
  eq(p.needed, false, 'a small image is uploaded untouched')
}

// The camera case: 12 MP, 8 MB. Scaled to the long-edge cap, aspect ratio preserved exactly.
{
  const p = resizePlan({ width: 4000, height: 3000, bytes: 8000000, type: 'image/jpeg' })
  eq(p.needed, true, 'a camera photo must be resized')
  eq(p.width, MAX_EDGE, 'longest edge is capped')
  eq(p.height, 1500, 'and the other edge scales proportionally')
  ok(Math.abs((p.width / p.height) - (4000 / 3000)) < 0.001, 'aspect ratio is preserved')
}

// Portrait — the orientation a phone actually shoots in. The CAP applies to the long edge, not to width.
{
  const p = resizePlan({ width: 3000, height: 4000, bytes: 8000000, type: 'image/jpeg' })
  eq(p.height, MAX_EDGE, 'portrait caps the height')
  eq(p.width, 1500, 'and scales the width')
}

// Over the byte ceiling but modest dimensions: re-encode at the same size rather than shrink it.
// Throwing away pixels when the fix is compression would degrade the image for no reason.
{
  const p = resizePlan({ width: 1200, height: 900, bytes: 3000000, type: 'image/jpeg' })
  eq(p.needed, true, 'over the ceiling, so something must happen')
  eq(p.width, 1200, 'dimensions kept')
  eq(p.height, 900, 'dimensions kept')
}

// Exactly at the limit is not over it.
{
  eq(resizePlan({ width: 100, height: 100, bytes: MAX_BYTES, type: 'image/jpeg' }).needed, false,
    'exactly at maxSize is allowed — the lexicon says maxSize, not "less than"')
  eq(resizePlan({ width: 100, height: 100, bytes: MAX_BYTES + 1, type: 'image/jpeg' }).needed, true,
    'one byte over is not')
}

// A non-image is never re-encoded — canvas would silently produce a broken picture.
{
  eq(resizePlan({ width: 0, height: 0, bytes: 9000000, type: 'application/pdf' }).needed, false,
    'a non-image is left for the caller to reject, not mangled')
}

// aspectRatio is what bsky lays the image out with, and we have never sent it. Integers, per the lexicon.
{
  const ar = aspectRatioOf(2000, 1500)
  eq(ar.width, 2000, 'aspectRatio carries the width')
  eq(ar.height, 1500, 'and the height')
  ok(Number.isInteger(ar.width) && Number.isInteger(ar.height), 'both are integers')
  eq(aspectRatioOf(0, 0), null, 'unknown dimensions produce NO claim rather than a zero ratio')
}

console.log(`\nRESIZE-PLAN OK: ${pass} assertions`)