Major update for v1 and tests

This commit is contained in:
khalid@traclabs.com
2026-05-23 03:28:58 -05:00
parent 628a6765f4
commit b1fb6fa3aa
1748 changed files with 27723 additions and 1854 deletions

103
src/utils/blobUrl.test.js Normal file
View File

@@ -0,0 +1,103 @@
// Tests for blobUrl.js — the isBlobUrl predicate and revokeBlobUrl
// wrapper used by persistence stripping and the photo-edit cleanup
// path.
//
// What this guards
// ────────────────
// isBlobUrl is consulted in two places that both have correctness
// consequences:
//
// 1. `persistence.savePersistedState` strips elements whose `src`
// is a blob: URL before saving to localStorage. Blob URLs go
// stale on reload — keeping them would produce broken-image
// elements on next session restore. If isBlobUrl returns false
// for a real blob URL, those broken images appear after refresh.
//
// 2. The photo-edit flow revokes the previous blob URL when the
// user applies an edit so the browser frees the underlying
// memory. If isBlobUrl returns true for a non-blob URL, we'd
// try to revoke a regular https:// URL, which is at best a no-op
// and at worst a deopt the browser logs.
//
// Both call sites benefit from a stable predicate, so the tests pin
// the exact behavior: string-typed AND starts with literal "blob:".
import { isBlobUrl, revokeBlobUrl } from './blobUrl';
describe('isBlobUrl', () => {
it('returns true for a string starting with "blob:"', () => {
// Real blob URLs look like blob:https://example.com/<uuid>. The
// predicate only checks the prefix, not the suffix shape — any
// string starting with "blob:" matches.
expect(isBlobUrl('blob:https://example.com/abc-123')).toBe(true);
expect(isBlobUrl('blob:')).toBe(true);
});
it('returns false for non-blob URLs', () => {
// These are the URL shapes we DON'T want to revoke:
// - data: URLs (survive a reload, persist fine)
// - https:// URLs (CDN-hosted images)
// - file:// URLs (local file pickers)
// - relative paths
expect(isBlobUrl('data:image/png;base64,iVBORw0KG')).toBe(false);
expect(isBlobUrl('https://example.com/photo.jpg')).toBe(false);
expect(isBlobUrl('file:///Users/khalid/photo.jpg')).toBe(false);
expect(isBlobUrl('/uploads/photo.jpg')).toBe(false);
expect(isBlobUrl('photo.jpg')).toBe(false);
});
it('returns false for non-strings (no throw)', () => {
// The docblock specifies: "Returns false for non-strings rather
// than throwing — callers usually test optional fields like
// `element.src` and shouldn't have to null-check twice." This
// is the regression test for that contract.
expect(isBlobUrl(null)).toBe(false);
expect(isBlobUrl(undefined)).toBe(false);
expect(isBlobUrl(42)).toBe(false);
expect(isBlobUrl({})).toBe(false);
expect(isBlobUrl([])).toBe(false);
});
it('returns false for the empty string', () => {
expect(isBlobUrl('')).toBe(false);
});
it('is case-sensitive ("BLOB:" is not "blob:")', () => {
// Real browser-issued blob URLs are always lowercase, so this
// is the desirable behavior. Pinning it here so a future
// "normalize case" refactor doesn't silently expand the
// predicate's surface to include uppercase variants we never
// intended to support.
expect(isBlobUrl('BLOB:https://example.com/abc')).toBe(false);
expect(isBlobUrl('Blob:https://example.com/abc')).toBe(false);
});
});
describe('revokeBlobUrl', () => {
// jsdom does not provide URL.revokeObjectURL by default. The
// function under test wraps the call in try/catch precisely to
// handle environments where it's missing (or where the URL has
// already been revoked). These tests verify the no-throw contract
// across the various inputs callers actually send.
it('does not throw for a blob: URL even if revokeObjectURL is missing', () => {
// The cleanup path in the photo editor will call this on every
// edit. It MUST NOT throw — that would interrupt the edit flow
// and leave the user staring at a half-applied state.
expect(() => revokeBlobUrl('blob:https://example.com/abc')).not.toThrow();
});
it('does not throw for non-blob URLs (no-op)', () => {
// Defensive: if a caller accidentally passes a data: URL, we
// shouldn't try to revoke it. The isBlobUrl guard inside
// revokeBlobUrl short-circuits this case.
expect(() => revokeBlobUrl('data:image/png;base64,abc')).not.toThrow();
expect(() => revokeBlobUrl('https://example.com/photo.jpg')).not.toThrow();
});
it('does not throw for non-string inputs', () => {
expect(() => revokeBlobUrl(null)).not.toThrow();
expect(() => revokeBlobUrl(undefined)).not.toThrow();
expect(() => revokeBlobUrl(42)).not.toThrow();
});
});