Files
apparel-designer/docs/BUGS.md
2026-05-23 03:28:58 -05:00

11 KiB
Raw Blame History

Apparel Designer — Bug Report

This is the active bug tracker. New regressions or freshly discovered defects belong here; once a bug is fixed, it moves to CHANGES.md (which is the historical record) and is removed from this file.


🔴 Critical

None outstanding.

🟠 Major

B1 — Cmd/Ctrl+D on a multi-selection produces N undo entries

File: src/App.jsx (keyboard-shortcut effect, around the Cmd+D branch)

The handler iterates the selection set and calls duplicateElement(id) once per id. Each duplicateElement call commits its own history entry, so duplicating 3 selected elements produces 3 separate undo steps — the user has to press Cmd-Z three times to "undo the duplicate" they just did. The single-select path is correct (one duplicate → one undo); only multi-select is wrong.

Fix shape: add duplicateMany(ids) to useDesignEditor that mirrors deleteMany's "single history entry, set selection to the new ids" pattern, and have the Cmd+D handler call that when selectedIds.size > 1.

🟡 Minor

B2 — [ / ] z-order shortcuts are inert in multi-select mode

File: src/App.jsx

The handler is gated on selectedId (the single-select derived value, which is null whenever selectedIds.size > 1). With multiple elements selected, [ and ] do nothing. Arrow-key nudging has the same gap — it operates on selectedId only.

Reasonable interpretations:

  • Iterate the selection (each element moves one step in z-order) — note that for adjacent selected layers this can produce confusing reorderings.
  • Treat the multi-selection as a contiguous block and shift it as a unit.

Either is a real call; the current behavior (silent no-op) is the worst.

A related stale-comment issue: the same handler still carries a comment "Cmd/Ctrl+A 'select all' is intentionally NOT bound here … makes sense once multi-select lands (S3)". S3 has now landed, so the comment misrepresents current state.

B3 — ImageElement.jsx filter useEffect over-invalidates on width/height changes

File: src/components/canvas/ImageElement.jsx

The effect's dep array is [filter, src, width, height], but the body only references filter and src. width/height are listed because Konva's filter cache is dimension-sensitive — but Konva ALSO re-caches automatically when the node's size changes, so listing them in the deps causes a redundant node.cache() + clearCache() cycle on every resize. During a continuous transformer drag this fires once per intermediate onUpdate, which on filtered images becomes a noticeable hot path.

Fix shape: drop width/height from the deps and rely on Konva's built-in re-cache. Verify with a brief drag of a filtered image that the filter still updates correctly.

B4 — useBackgroundRemoval.removeBackground early-return doesn't toggle loading

File: src/hooks/useBackgroundRemoval.js

When loadModel() fails (network error fetching the 86MB model), the early-return path returns null without calling setLoading(false). loadModel itself does call setLoading(false) in its catch block, so today the user-visible state is consistent — but the contract between the two functions is fragile: if loadModel is ever changed to return false without resetting loading, this caller silently strands the spinner.

Fix shape: defensive setLoading(false) in the early-return path of removeBackground.

B5 — useBackgroundRemoval progress is non-monotonic and confusing

File: src/hooks/useBackgroundRemoval.js

Model-download phase reports 050 (scaled from the HF progress callback). Then removeBackground jumps straight to setProgress(50), then 70, 90, 100 at fixed pipeline checkpoints. The 50→70→90 jumps are visible to the user as a stuttering bar that pauses, leaps, pauses, leaps. It still finishes — just looks broken.

Fix shape: either (a) replace the progress bar with an indeterminate spinner once the model is loaded — the per-image inference is fast enough that fake-stepping doesn't help, or (b) genuinely time-budget the post-load steps and report linearly.

B6 — UploadTab.jsx uses bare alert() for validation errors

File: src/components/sidebar/UploadTab.jsx

Three alert() calls — file-type rejection, file-size rejection, upload-failure — bypass the app-wide toast system that #19 introduced. These are the only alert() calls left in the codebase. The toast system has the right "info / error" kinds for these messages.

Fix shape: thread showToast into UploadTab (via Sidebar prop drilling, or via a context) and replace the alert calls. Note: this IS a user-visible change (alert vs toast looks different) — log it here rather than fix immediately, per the "no user-facing changes" instruction.

B7 — UploadTab.placeImage hard-codes 300 for canvas centering

File: src/components/sidebar/UploadTab.jsx

The drop-zone path computes (300 - width) / 2 and (300 - height) / 2 to center an uploaded photo. 300 is the current canvasSize, but getActiveProduct().canvasSize is the source of truth. Drift risk: if the active product is ever swapped to one with a different canvas size, uploaded photos won't center correctly.

Fix shape: import { getActiveProduct } from '../../constants/products' and read canvasSize once at module top (or per-call).

B8 — OfflineIndicator initializes state without an SSR guard

File: src/components/OfflineIndicator.jsx

useState(!navigator.onLine) runs at module-execution time inside the function component. There's no guard for a missing navigator. The app isn't SSR'd today, but this is the pattern that breaks first if anyone tries — and it's gratuitous; the rest of the codebase consistently guards (typeof window === 'undefined').

Fix shape: useState(() => typeof navigator !== 'undefined' ? !navigator.onLine : false).

B9 — SlotPlaceholder mouseLeave forces cursor: 'default'

File: src/components/canvas/SlotPlaceholder.jsx

handleMouseLeave sets stage.container().style.cursor = 'default'. This stomps on whatever cursor was inherited (e.g. another shape that sets 'pointer' via its own enter handler may not get to restore on the next move event because we just hard-set 'default'). The standard idiom is 'auto' or '' to clear the inline style and fall back to the inherited cascade.

In practice the bug is hard to trigger today (the only other shape-level cursor handler IS this same one), so it's classified Minor — but it's a latent foot-gun.

B10 — persistence.js stripUnpersistable filter narrowness

File: src/utils/persistence.js

The filter only inspects type === 'image' elements. Today stickers (type: 'sticker') have data URLs, not blob URLs, so this works. But the contract isn't symmetric with reality — if any future code path produces a sticker with a blob: src (e.g. a sticker authored from a blob URL on first paste), it won't be filtered, and reload will produce a broken sticker.

Fix shape: filter on src URL prefix, not type — if (typeof el.src === 'string' && el.src.startsWith('blob:')) drop. Same intent, narrower-than-needed gate today.

B11 — useExport progress simulator is misleading

File: src/hooks/useExport.js

progressInterval advances progress 0→90 in 10% steps every 200ms while the export is in flight, regardless of actual upload/render progress. On a fast export, the user sees 30% before the request even arrives at the server. On a slow export, the bar pegs at 90% and waits silently for whatever's actually slow.

Two real fixes:

  1. Wire to fetch's upload-progress events (XHR has them; fetch+ReadableStream support is patchy but adequate for our use case). Server-render time is opaque from the client, so progress will still cap at "uploaded" and then sit indeterminate.
  2. Replace the bar with an indeterminate spinner — honest about the fact that we don't know how far along the server is.

B12 — handleShare has hardcoded English strings not in the i18n catalog

File: src/App.jsx (handleShare)

'My Pawfectly Yours design' (share title) and 'Check out the shirt I made!' (share text) are passed directly to navigator.share. The S23 catalog covers toast messages and button labels but missed these because they don't render in the DOM — they go straight to the OS share sheet. Should be added to the catalog and pulled via t().

Same applies to several inline strings in App.jsx render — FAB aria-labels ('Close customization options' / 'Open customization options'), export-toast verbiage ('Saving design…', '⚠️ Save failed:', '✅ Saved!', 'Download', 'Dismiss'). All logged collectively under the S10 i18n-sweep follow-on, but B12 specifically calls out the share strings since they're easy to miss in a DOM-text grep.


Closed since the May 2026 audit

The original May 2026 audit identified twelve critical (C1C12), seven major (M1M7), and nine minor (m1m9) bugs. All of them have been fixed. See CHANGES.md for the per-bug summary, the files touched, and the verification steps. The performance contract for the drag/rotate hot path (M4) is the most operationally important section — it documents the constraints any future change to DesignCanvas.jsx's bound functions must respect.

Subsequent fix rounds (the polish/UX work tracked outside this file under issue numbers like #1#35) are also captured in CHANGES.md. Items addressed there but originally listed in SUGGESTIONS.md have been removed from that file too.


Notes on items deliberately not flagged as bugs

These are observations from the audit that were not bugs at the time and are kept here as ongoing watch-items so future contributors don't re-audit ground we've already considered.

  • Express 4 wildcard routes (app.all('/api/*'), app.get('*')) are correct for the pinned ^4.18.2. These would break under Express 5's path-to-regexp v6 and are worth revisiting if/when the project upgrades, but they aren't bugs today.
  • canvas package version ^2.11.2 is older than the current 3.x line. 2.x is still maintained and works; mentioning here only for transparency.
  • CORS in dev allows all origins — intentional, gated on IS_PRODUCTION. Not a bug.
  • saveToHistory JSON-stringify dedupe — uses string equality on JSON.stringify(elements), which is sensitive to numeric formatting (0 vs 0.0, key ordering between Object.assign results, etc.). In practice React's setState updaters produce stable object shapes so this is fine, but if a future code path constructs elements differently, identical-content snapshots could end up as separate history entries. Worth keeping aware of, not worth fixing speculatively.
  • useBackgroundRemoval.hasModel — returned from the hook but ignored by every consumer (only BackgroundRemovalButton calls the hook). Not a user-visible bug; logged as S21 in suggestions for cleanup.
  • useTemplate.templateRef — set in loadTemplate and clearTemplate but never read anywhere. Dead state; logged as S22 in suggestions.