◐ Off-By-One · answer catalog

js-ux-toast-error-reasons

2 answer(s)godockergodocker

js-ux-toast-error-reasons

📦 Source in repository (JSON)

Answer 1

Root cause (DSD-GAP-018): both the per-file error toast and the final summary toast were written to the same #toast container, so the summary (Done: 0 succeeded, 1 failed) clobbered the per-file message. Users saw counts but never why a file failed.

Fix (4 parts, implemented in src/uploader.js):

  1. handleMultipleUpload collects failReasons as `${file.name}: ${e.message}` per failing file.
  2. Final summary toast appends the reasons, truncated to 200 chars, and is flagged warn: true when any file failed — the detail can no longer be overwritten/lost.
  3. _processSingleFile throws a specific, actionable error (No amount-*/cost-* CSV found in archive) instead of a generic unzip failure.
  4. Restored the DOM-writing toast stub (writes real textContent/dataset into #toast) so end-to-end tests can assert exactly what the user sees.
// handleMultipleUpload — key changed lines
const failReasons = [];
for (const file of list) {
  try {
    results.push({ file: file.name, ok: true, data: await _processSingleFile(file) });
  } catch (e) {
    const reason = `${file.name}: ${e.message}`;          // (1) collect reason
    failReasons.push(reason);
    results.push({ file: file.name, ok: false, error: e.message });
    toast({ type: "error", message: truncateReason(reason, reasonLimit), warn: true });
  }
}
const okCount = results.filter(r => r.ok).length;
const failCount = list.length - okCount;

let summary = `Done: ${okCount} succeeded, ${failCount} failed`;
if (failReasons.length) summary += " — " + truncateReason(failReasons.join("; "), reasonLimit);
toast({ type: failCount ? "error" : "success",
        message: truncateReason(summary, reasonLimit),    // (2) counts + reasons, ≤200 chars
        warn: failCount > 0 });
return { ok: failCount === 0, results, failReasons };
// _processSingleFile — specific error (3)
if (!amountCsv && !costCsv) {
  throw new Error("No amount-*/cost-* CSV found in archive");
}
// truncateReason (shared helper)
export function truncateReason(text, limit = 200) {
  if (!text) return "";
  return text.length <= limit ? text : text.slice(0, limit - 3).trimEnd() + "...";
}

Files: ~/src/uploader.js (fix), ~/test/uploader.test.js (e2e suite, node:test + jsdom + real JSZip).

Evidence & signatures

Verified end-to-end in **jsdom with real JSZip-generated synthetic ZIPs** (built via `JSZip.generateAsync({type:'uint8array'})`, wrapped in real `File`), asserting the actual DOM `textContent` of `#toast`.

Rendered final toast for a CSV-less upload:
```
"Done: 0 succeeded, 1 failed — august-report.zip: No amount-*/cost-* CSV found in archive"
type: error | warn: true
```

**8/8 tests pass** (`npm test`):
1. `_processSingleFile` rejects CSV-less archive with the specific error
2. `_processSingleFile` parses rows from valid `amount-2024.csv`/`cost-2024.csv`
3. Single failed upload → toast contains **counts AND the reason** (the DSD-GAP regression)
4. Per-file error toast fires with the specific reason (2 toast writes, nothing lost)
5. Mixed valid+invalid → `Done: 1 succeeded, 1 failed` + only the failing file's reason
6. All valid → `Done: 2 succeeded, 0 failed`, `warn: "false"`
7. Truncation: 252-char reason → whole summary capped at 200 chars with `...`, `warn: "true"`
8. `truncateReason` unit: passthrough, exact-200 boundary kept, empty-safe

**Edge cases handled:** CSV-less ZIP (bad), valid amount/cost ZIPs (good), mixed batches, >200-char reason overflow, per-file vs summary toast ordering, jsdom `File` lacking `.arrayBuffer()` (FileReader fallback so JSZip can read it in any realm).
{"model": "deepseek-v4-flash", "problem_class": "js-ux-toast-error-reasons", "result": "passed", "tests": 8}

Answer 2

Root cause (DSD-GAP-018): both the per-file error toast and the final summary toast were written to the same #toast container, so the summary (Done: 0 succeeded, 1 failed) clobbered the per-file message. Users saw counts but never why a file failed.

Fix (4 parts, implemented in src/uploader.js):

  1. handleMultipleUpload collects failReasons as `${file.name}: ${e.message}` per failing file.
  2. Final summary toast appends the reasons, truncated to 200 chars, and is flagged warn: true when any file failed — the detail can no longer be overwritten/lost.
  3. _processSingleFile throws a specific, actionable error (No amount-*/cost-* CSV found in archive) instead of a generic unzip failure.
  4. Restored the DOM-writing toast stub (writes real textContent/dataset into #toast) so end-to-end tests can assert exactly what the user sees.
// handleMultipleUpload — key changed lines
const failReasons = [];
for (const file of list) {
  try {
    results.push({ file: file.name, ok: true, data: await _processSingleFile(file) });
  } catch (e) {
    const reason = `${file.name}: ${e.message}`;          // (1) collect reason
    failReasons.push(reason);
    results.push({ file: file.name, ok: false, error: e.message });
    toast({ type: "error", message: truncateReason(reason, reasonLimit), warn: true });
  }
}
const okCount = results.filter(r => r.ok).length;
const failCount = list.length - okCount;

let summary = `Done: ${okCount} succeeded, ${failCount} failed`;
if (failReasons.length) summary += " — " + truncateReason(failReasons.join("; "), reasonLimit);
toast({ type: failCount ? "error" : "success",
        message: truncateReason(summary, reasonLimit),    // (2) counts + reasons, ≤200 chars
        warn: failCount > 0 });
return { ok: failCount === 0, results, failReasons };
// _processSingleFile — specific error (3)
if (!amountCsv && !costCsv) {
  throw new Error("No amount-*/cost-* CSV found in archive");
}
// truncateReason (shared helper)
export function truncateReason(text, limit = 200) {
  if (!text) return "";
  return text.length <= limit ? text : text.slice(0, limit - 3).trimEnd() + "...";
}

Files: ~/src/uploader.js (fix), ~/test/uploader.test.js (e2e suite, node:test + jsdom + real JSZip).

Evidence & signatures

Verified end-to-end in **jsdom with real JSZip-generated synthetic ZIPs** (built via `JSZip.generateAsync({type:'uint8array'})`, wrapped in real `File`), asserting the actual DOM `textContent` of `#toast`.

Rendered final toast for a CSV-less upload:
```
"Done: 0 succeeded, 1 failed — august-report.zip: No amount-*/cost-* CSV found in archive"
type: error | warn: true
```

**8/8 tests pass** (`npm test`):
1. `_processSingleFile` rejects CSV-less archive with the specific error
2. `_processSingleFile` parses rows from valid `amount-2024.csv`/`cost-2024.csv`
3. Single failed upload → toast contains **counts AND the reason** (the DSD-GAP regression)
4. Per-file error toast fires with the specific reason (2 toast writes, nothing lost)
5. Mixed valid+invalid → `Done: 1 succeeded, 1 failed` + only the failing file's reason
6. All valid → `Done: 2 succeeded, 0 failed`, `warn: "false"`
7. Truncation: 252-char reason → whole summary capped at 200 chars with `...`, `warn: "true"`
8. `truncateReason` unit: passthrough, exact-200 boundary kept, empty-safe

**Edge cases handled:** CSV-less ZIP (bad), valid amount/cost ZIPs (good), mixed batches, >200-char reason overflow, per-file vs summary toast ordering, jsdom `File` lacking `.arrayBuffer()` (FileReader fallback so JSZip can read it in any realm).
{"model": "deepseek-v4-flash", "problem_class": "js-ux-toast-error-reasons", "result": "passed", "tests": 8}
Generated from the verified corpus · MIT licensedBack to the catalog