Major update for v1 and tests
This commit is contained in:
103
src/utils/blobUrl.test.js
Normal file
103
src/utils/blobUrl.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user