Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ import { toast } from "sonner";
const { error } = await supabase.from("pages").insert({ ... });
if (error) {
captureSupabaseError(error, "pages.insert");
toast.error("Failed to create page");
toast.error("Failed to create page", { duration: 8000 });
}
```

Expand Down
2 changes: 1 addition & 1 deletion src/components/page-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function PageMenu({
const handleExport = useCallback(() => {
const editor = editorRef.current;
if (!editor) {
toast.error("Editor not ready");
toast.error("Editor not ready", { duration: 8000 });
return;
}

Expand Down
16 changes: 8 additions & 8 deletions src/components/sidebar/page-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:fetch-pages");
toast.error("Failed to load pages");
toast.error("Failed to load pages", { duration: 8000 });
}

if (data) {
Expand Down Expand Up @@ -189,7 +189,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:create-page");
toast.error("Failed to create page");
toast.error("Failed to create page", { duration: 8000 });
return;
}
if (!newPage) return;
Expand All @@ -215,7 +215,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:delete-page");
toast.error("Failed to delete page");
toast.error("Failed to delete page", { duration: 8000 });
} else {
const removedIds = new Set([
deleteTarget.page.id,
Expand Down Expand Up @@ -275,7 +275,7 @@ export function PageTree({ userId }: PageTreeProps) {
for (const result of results) {
if (result.error) {
captureSupabaseError(result.error, "page-tree:swap-positions");
toast.error("Failed to reorder page");
toast.error("Failed to reorder page", { duration: 8000 });
break;
}
}
Expand Down Expand Up @@ -315,7 +315,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:nest-page");
toast.error("Failed to nest page");
toast.error("Failed to nest page", { duration: 8000 });
}
}

Expand Down Expand Up @@ -367,7 +367,7 @@ export function PageTree({ userId }: PageTreeProps) {
for (const result of results) {
if (result.error) {
captureSupabaseError(result.error, "page-tree:unnest-page");
toast.error("Failed to unnest page");
toast.error("Failed to unnest page", { duration: 8000 });
break;
}
}
Expand Down Expand Up @@ -455,7 +455,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:drop-inside");
toast.error("Failed to move page");
toast.error("Failed to move page", { duration: 8000 });
}
} else {
const newParentId = targetPage.parent_id;
Expand Down Expand Up @@ -493,7 +493,7 @@ export function PageTree({ userId }: PageTreeProps) {

if (error) {
captureSupabaseError(error, "page-tree:drop-reorder");
toast.error("Failed to move page");
toast.error("Failed to move page", { duration: 8000 });
break;
}
}
Expand Down
63 changes: 63 additions & 0 deletions src/components/toast-error-duration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync, statSync } from "fs";
import { join, relative } from "path";

/**
* Regression test for issue #87: toast.error() calls must specify
* { duration: 8000 } per the design spec (8 seconds for errors).
*
* Scans all .tsx/.ts source files under src/ for toast.error() calls
* and verifies each one includes the duration option.
*/

function collectFiles(dir: string, ext: string[]): string[] {
const results: string[] = [];
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
if (entry === "node_modules" || entry === ".next") continue;
results.push(...collectFiles(full, ext));
} else if (ext.some((e) => full.endsWith(e))) {
results.push(full);
}
}
return results;
}

describe("toast.error() duration matches design spec", () => {
const srcDir = join(__dirname, "..");
const files = collectFiles(srcDir, [".ts", ".tsx"]).filter(
(f) => !f.endsWith(".test.ts") && !f.endsWith(".test.tsx")
);

it("every toast.error() call includes { duration: 8000 }", () => {
const violations: string[] = [];

for (const file of files) {
const content = readFileSync(file, "utf-8");
const lines = content.split("\n");

for (let i = 0; i < lines.length; i++) {
if (!lines[i].includes("toast.error(")) continue;

// Collect the full statement (may span multiple lines)
let statement = lines[i];
let j = i;
while (j < lines.length - 1 && !statement.includes(");")) {
j++;
statement += " " + lines[j];
}

if (!statement.includes("duration: 8000")) {
const rel = relative(srcDir, file);
violations.push(`${rel}:${i + 1}: ${lines[i].trim()}`);
}
}
}

expect(
violations,
`toast.error() calls missing { duration: 8000 } (design spec: 8s for errors):\n${violations.join("\n")}`
).toHaveLength(0);
});
});
2 changes: 1 addition & 1 deletion src/components/workspace-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function WorkspaceHome({

if (error) {
captureSupabaseError(error, "workspace-home:create-page");
toast.error("Failed to create page");
toast.error("Failed to create page", { duration: 8000 });
return;
}
if (!newPage) return;
Expand Down
Loading