Skip to content

Comments

fix: Plain chat not opening from help button for free users and on bookings list page#23491

Merged
anikdhabal merged 3 commits intomainfrom
fix/plain_not_opening_free_users
Sep 1, 2025
Merged

fix: Plain chat not opening from help button for free users and on bookings list page#23491
anikdhabal merged 3 commits intomainfrom
fix/plain_not_opening_free_users

Conversation

@Amit91848
Copy link
Member

What does this PR do?

  • Fixes #XXXX (GitHub issue number)
  • Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

Visual Demo (For contributors especially)

BEFORE:

Screen.Recording.2025-09-01.at.5.00.22.PM.mov

AFTER:

Screen.Recording.2025-09-01.at.4.58.30.PM.mov

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 1, 2025

Walkthrough

PlainContactForm was refactored to be a controlled component accepting props { open, setIsOpen } instead of managing internal isOpen. The form now tracks message, attachmentIds, and uploads state, supports file uploads via /api/support/upload (using returned uploadFormUrl and uploadFormData), and submits payloads to /api/support. UI changes include Label, TextArea, FileUploader (images/videos, max 5 files, 10MB each), loader on submit, a post-submission “Message Sent” view, and a testId on the uploader. plainChat adds isOpen for non-paid flows and stubs window.Plain; init logic runs only for paid users. useIsBookingPage now excludes booking-list routes. UserDropdown.handleHelpClick now prevents event propagation, closes the menu, and defers opening Plain via nested requestAnimationFrame.

Possibly related PRs

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/plain_not_opening_free_users

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@graphite-app graphite-app bot requested a review from a team September 1, 2025 11:31
@keithwillcode keithwillcode added the core area: core, team members only label Sep 1, 2025
Comment on lines +60 to +70
e?.preventDefault();
e?.stopPropagation();

setMenuOpen(false);

// Defer to next frame to avoid the originating click closing the dialog
requestAnimationFrame(() => {
// double RAF is extra-safe if your dropdown unmounts with a transition
requestAnimationFrame(() => {
if (window.Plain) window.Plain.open();
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoids closing the PlainContactForm component

@dosubot dosubot bot added the 🐛 bug Something isn't working label Sep 1, 2025
anikdhabal
anikdhabal previously approved these changes Sep 1, 2025
@anikdhabal anikdhabal enabled auto-merge (squash) September 1, 2025 11:37
@vercel
Copy link

vercel bot commented Sep 1, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Sep 1, 2025 11:54am
cal-eu Ignored Ignored Sep 1, 2025 11:54am

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
apps/web/components/plain/PlainContactForm.tsx (3)

69-73: Fix upload error handling: await response text, don’t stop “uploading” early, and clean up failed placeholders.

  • uploadRes.text must be awaited.
  • setIsUploadingImage(false) is called before the actual upload; defer until success/failure.
  • Remove the optimistic placeholder on pre-upload failure.

Apply this diff:

@@
-      setIsUploadingImage(false);
+      // keep uploading state until the actual file POST completes
@@
-      if (!res.ok) {
-        showToast("Error uploading attachment", "error");
-        setIsUploadingImage(false);
-        return;
-      }
+      if (!res.ok) {
+        showToast("Error uploading attachment", "error");
+        // remove optimistic entry
+        setUploads((prev) => prev.filter((u) => u.id !== newFile.id));
+        setIsUploadingImage(false);
+        return;
+      }
@@
-      if (!uploadRes.ok) {
-        showToast(`Failed while uploading file: ${uploadRes.text}`, "error");
-        setIsUploadingImage(false);
-        return;
-      }
+      if (!uploadRes.ok) {
+        const errText = await uploadRes.text().catch(() => uploadRes.statusText);
+        showToast(`Failed while uploading file: ${errText}`, "error");
+        // remove optimistic entry
+        setUploads((prev) => prev.filter((u) => u.id !== newFile.id));
+        setIsUploadingImage(false);
+        return;
+      }

Also applies to: 55-55, 43-47


34-37: Process all added/removed files; don’t ignore subsequent items.

Currently only the first item of newFiles/removedFiles is handled.

Apply this diff:

-  const handleUpload = async (allFiles: FileData[], newFiles: FileData[], removedFiles: FileData[]) => {
-    if (newFiles.length > 0) {
-      const newFile = newFiles[0];
+  const handleUpload = async (_allFiles: FileData[], newFiles: FileData[], removedFiles: FileData[]) => {
+    // Handle added files
+    for (const newFile of newFiles) {
       setUploads((prev) => [...prev, { file: newFile.file, uploading: true, id: newFile.id }]);
       setIsUploadingImage(true);
@@
-      setUploads((prev) =>
-        prev.map((upload) => (upload.file === file ? { ...upload, uploading: false, attachmentId } : upload))
-      );
+      setUploads((prev) =>
+        prev.map((u) => (u.id === newFile.id ? { ...u, uploading: false, attachmentId } : u))
+      );
       setData((prev) => ({
         ...prev,
         attachmentIds: [...prev.attachmentIds, attachmentId],
       }));
       setIsUploadingImage(false);
       showToast("File uploaded successfully", "success");
-    } else if (removedFiles.length > 0) {
-      const removedFile = removedFiles[0];
-      const file = uploads.find((upload) => upload.id === removedFile.id);
-      if (!file) {
-        console.warn("File not found in uploads: ", removedFile.id);
-        return;
-      }
-      setData((prev) => ({
-        ...prev,
-        attachmentIds: prev.attachmentIds.filter((id) => id !== file.attachmentId),
-      }));
-      setUploads((prev) => prev.filter((upload) => upload.id !== removedFile.id));
-    }
+    }
+    // Handle removed files
+    for (const removedFile of removedFiles) {
+      const entry = uploads.find((u) => u.id === removedFile.id);
+      if (!entry) continue;
+      setData((prev) => ({
+        ...prev,
+        attachmentIds: prev.attachmentIds.filter((id) => id !== entry.attachmentId),
+      }));
+      setUploads((prev) => prev.filter((u) => u.id !== removedFile.id));
+    }

Also applies to: 75-82, 84-97


196-199: Mismatch: multiple=false but maxFiles=5.

Either set multiple to true or reduce maxFiles to 1. Likely the intent is multiple attachments.

Apply this diff:

-                    multiple={false}
+                    multiple
                     showFilesList
-                    maxFiles={5}
+                    maxFiles={5}
apps/web/components/plain/plainChat.tsx (1)

252-259: Async cleanup from initConfig is ignored by useEffect — timer leak risk.

Returning a cleanup function from an async function doesn’t register with useEffect; the setTimeout may survive unmounts.

Apply this diff within the range to store the timer and avoid returning a cleanup from the async function:

-          if (shouldOpenPlain) {
-            const timer = setTimeout(() => {
-              if (window.Plain) {
-                window.Plain.open();
-              }
-            }, 100);
-            return () => clearTimeout(timer);
-          }
+          if (shouldOpenPlain) {
+            // Defer open; cleanup handled in the effect's cleanup via openTimerRef
+            openTimerRef.current = window.setTimeout(() => {
+              if (window.Plain) {
+                window.Plain.open();
+              }
+            }, 100);
+          }

And update the effect’s cleanup to clear the stored timer:

-        return () => window.removeEventListener("resize", checkScreenSize);
+        return () => {
+          window.removeEventListener("resize", checkScreenSize);
+          if (openTimerRef.current) {
+            clearTimeout(openTimerRef.current);
+            openTimerRef.current = null;
+          }
+        };

Add the supporting pieces outside this range:

// import
import { useEffect, useState, useCallback, useMemo, useRef } from "react";

// inside component, near other state
const openTimerRef = useRef<number | null>(null);
🧹 Nitpick comments (7)
packages/features/shell/user-dropdown/UserDropdown.tsx (1)

3-3: Type nit + safer call: import MouseEvent and use optional chaining.

Minor polish to avoid relying on the React namespace and to guard the call.

Apply this diff:

-import { useEffect, useState } from "react";
+import { useEffect, useState, type MouseEvent } from "react";
@@
-  const handleHelpClick = (e?: React.MouseEvent) => {
+  const handleHelpClick = (e?: MouseEvent) => {
@@
-        if (window.Plain) window.Plain.open();
+        window.Plain?.open();

Also applies to: 59-71

apps/web/lib/hooks/useIsBookingPage.ts (1)

7-16: Micro: hoist static route arrays to module scope.

Avoids re-allocating arrays on each render.

Apply this diff:

+const BOOKING_ROUTES = [
+  "/booking",
+  "/cancel",
+  "/reschedule",
+  "/instant-meeting", // Instant booking page
+  "/team", // Team booking pages
+  "/d", // Private Link of booking page
+  "/apps/routing-forms/routing-link", // Routing Form page
+  "/forms/", // Rewrites to /apps/routing-forms/routing-link
+] as const;
+
+const BOOKING_LIST_SUFFIXES = ["/upcoming", "/unconfirmed", "/recurring", "/cancelled", "/past"] as const;
@@
-  const isBookingPage = [
-    "/booking",
-    "/cancel",
-    "/reschedule",
-    "/instant-meeting", // Instant booking page
-    "/team", // Team booking pages
-    "/d", // Private Link of booking page
-    "/apps/routing-forms/routing-link", // Routing Form page
-    "/forms/", // Rewrites to /apps/routing-forms/routing-link
-  ].some((route) => pathname?.startsWith(route));
-  const isBookingsListPage = ["/upcoming", "/unconfirmed", "/recurring", "/cancelled", "/past"].some((route) => pathname?.endsWith(route));
+  const isBookingPage = BOOKING_ROUTES.some((route) => pathname?.startsWith(route));
+  const isBookingsListPage = BOOKING_LIST_SUFFIXES.some((route) => pathname?.endsWith(route));

Also applies to: 17-17

apps/web/components/plain/PlainContactForm.tsx (3)

57-61: Remove any-cast and type uploadFormData.

This avoids the eslint suppression and tightens the contract.

Apply this diff:

-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      uploadFormData.forEach(({ key, value }: any) => {
-        formData.append(key, value);
-      });
+      type UploadField = { key: string; value: string };
+      (uploadFormData as UploadField[]).forEach(({ key, value }) => {
+        formData.append(key, value);
+      });

141-141: Reset the form when the popover closes.

Prevents reopening to a stale “Message Sent” state.

Apply this diff:

-      <Popover open={open} onOpenChange={setIsOpen}>
+      <Popover
+        open={open}
+        onOpenChange={(next) => {
+          setIsOpen(next);
+          if (!next) resetForm();
+        }}>

Also applies to: 130-137


155-161: Accessibility: add an aria-label to the close button.

Improves screen reader UX.

Apply this diff:

-            <Button
+            <Button
               color="minimal"
               variant="button"
               StartIcon="x"
               size="sm"
+              aria-label={t("close")}
               onClick={() => setIsOpen(false)}
             />
apps/web/components/plain/plainChat.tsx (2)

273-283: Stub for free users: also honor query‑param auto‑open.

Mirror the paid flow so “?openPlain” opens the free contact form as well.

Apply:

-        } else if (typeof window !== "undefined" && !window.Plain) {
+        } else if (typeof window !== "undefined" && !window.Plain) {
           window.Plain = {
             // eslint-disable-next-line @typescript-eslint/no-empty-function
             init: () => {},
             open: () => {
               setIsOpen(true);
             }
           };
+          // Parity with paid users
+          if (shouldOpenPlain) {
+            window.Plain.open();
+          }
         }

Note: After using shouldOpenPlain here, include it in the dependency list (see next comment).


285-285: Effect deps: remove redundant userEmail; include shouldOpenPlain if used above.

userEmail is already captured by initConfig; adding it here causes extra runs. If you adopt the auto‑open change, add shouldOpenPlain.

-      }, [isAppDomain, checkScreenSize, initConfig, userEmail, isPaidUser]);
+      }, [isAppDomain, checkScreenSize, initConfig, isPaidUser, shouldOpenPlain]);
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f859c28 and ffe373f.

📒 Files selected for processing (4)
  • apps/web/components/plain/PlainContactForm.tsx (4 hunks)
  • apps/web/components/plain/plainChat.tsx (3 hunks)
  • apps/web/lib/hooks/useIsBookingPage.ts (1 hunks)
  • packages/features/shell/user-dropdown/UserDropdown.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • packages/features/shell/user-dropdown/UserDropdown.tsx
  • apps/web/components/plain/PlainContactForm.tsx
  • apps/web/components/plain/plainChat.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/shell/user-dropdown/UserDropdown.tsx
  • apps/web/lib/hooks/useIsBookingPage.ts
  • apps/web/components/plain/PlainContactForm.tsx
  • apps/web/components/plain/plainChat.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/shell/user-dropdown/UserDropdown.tsx
  • apps/web/lib/hooks/useIsBookingPage.ts
  • apps/web/components/plain/PlainContactForm.tsx
  • apps/web/components/plain/plainChat.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • apps/web/lib/hooks/useIsBookingPage.ts
🧠 Learnings (1)
📚 Learning: 2025-08-29T22:57:30.345Z
Learnt from: bandhan-majumder
PR: calcom/cal.com#23454
File: packages/features/form-builder/FormBuilder.tsx:11-11
Timestamp: 2025-08-29T22:57:30.345Z
Learning: FormBuilder.tsx in packages/features/form-builder/ does not have "use client" directive at the top despite using client-side React hooks and event handlers, which suggests it should be a client component.

Applied to files:

  • apps/web/components/plain/PlainContactForm.tsx
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (4)
packages/features/shell/user-dropdown/UserDropdown.tsx (1)

59-71: Deferring Plain.open with double rAF to avoid menu close race is solid.

Stopping propagation + closing the menu before opening the widget is the right sequence here. LGTM.

apps/web/lib/hooks/useIsBookingPage.ts (1)

17-17: Correctly excludes booking list routes from booking pages.

The endsWith check for list pages and the updated return condition are accurate for URLs like /bookings/past. LGTM.

Also applies to: 23-23

apps/web/components/plain/plainChat.tsx (2)

86-86: Controlled open state for free‑flow — good.

This enables external control and aligns with the refactored PlainContactForm.


301-303: LGTM — passes controlled props to PlainContactForm.

This correctly hooks the non‑paid flow into the shared UI.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/components/plain/__tests__/PlainContactForm.test.tsx (1)

119-135: Avoid persistent never-resolving mock and remove unnecessary await

Use a one-shot impl so later tests aren’t affected; toBeDisabled isn’t async.

-mockFetch.mockImplementation(() => new Promise((_resolve) => {}));
+mockFetch.mockImplementationOnce(() => new Promise((_resolve) => {}));

-await expect(submitButton).toBeDisabled();
+expect(submitButton).toBeDisabled();
🧹 Nitpick comments (6)
apps/web/components/plain/__tests__/PlainContactForm.test.tsx (6)

45-50: Disambiguate launcher button query

Use an accessible name to avoid matching the wrong button if more are rendered.

-const button = screen.getByRole("button");
+const button = screen.getByRole("button", { name: /contact/i });

52-60: Also assert call count and prefer named query

Prevents false positives and ensures a single open action.

-const button = screen.getByRole("button");
+const button = screen.getByRole("button", { name: /contact/i });
 fireEvent.click(button);

-expect(mockSetIsOpen).toHaveBeenCalledWith(true);
+expect(mockSetIsOpen).toHaveBeenCalledTimes(1);
+expect(mockSetIsOpen).toHaveBeenCalledWith(true);

76-86: Target the close button by accessible name instead of DOM shape

Querying SVG internals is brittle. Prefer role+name and assert presence.

-const buttons = screen.getAllByRole("button");
-const closeButton = buttons.find((button) => button.querySelector('svg use[href="#x"]'));
-expect(closeButton).toBeDefined();
-fireEvent.click(closeButton!);
+const closeButton = screen.getByRole("button", { name: /close|dismiss/i });
+expect(closeButton).toBeInTheDocument();
+fireEvent.click(closeButton);
 
 expect(mockSetIsOpen).toHaveBeenCalledWith(false);

88-117: Make fetch expectation stricter yet resilient

Assert one call and match only the important parts to reduce brittleness if headers/body gain fields.

-expect(mockFetch).toHaveBeenCalledWith("/api/support", {
-  method: "POST",
-  headers: {
-    "Content-Type": "application/json",
-  },
-  body: JSON.stringify({
-    message: "Test message",
-    attachmentIds: [],
-  }),
-});
+expect(mockFetch).toHaveBeenCalledTimes(1);
+expect(mockFetch).toHaveBeenNthCalledWith(
+  1,
+  "/api/support",
+  expect.objectContaining({
+    method: "POST",
+    headers: expect.objectContaining({ "Content-Type": "application/json" }),
+    body: JSON.stringify(
+      expect.objectContaining({
+        message: "Test message",
+        attachmentIds: [],
+      })
+    ),
+  })
+);

Consider adding a failure-path test asserting showToast is called. I can draft it if you want.


136-161: Optional: assert full reset of UI

After “Send another message”, also verify the submit button label resets and any attachments state clears (e.g., via uploader testId).


176-181: Use toBeRequired() and drop await

Cleaner intent and correct sync matcher usage.

-await expect(screen.getByLabelText("Describe the issue")).toHaveAttribute("required");
+expect(screen.getByLabelText("Describe the issue")).toBeRequired();
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ffe373f and d61403f.

📒 Files selected for processing (1)
  • apps/web/components/plain/__tests__/PlainContactForm.test.tsx (5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Always use t() for text localization in frontend code; direct text embedding should trigger a warning

Files:

  • apps/web/components/plain/__tests__/PlainContactForm.test.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • apps/web/components/plain/__tests__/PlainContactForm.test.tsx
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • apps/web/components/plain/__tests__/PlainContactForm.test.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-27T13:32:46.887Z
Learnt from: supalarry
PR: calcom/cal.com#23364
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/transformers/internal-to-api/internal-to-api.spec.ts:295-296
Timestamp: 2025-08-27T13:32:46.887Z
Learning: In calcom/cal.com, when transforming booking fields from internal to API format, tests in organizations-event-types.e2e-spec.ts already expect name field label and placeholder to be empty strings ("") rather than undefined. PR changes that set these to explicit empty strings are typically fixing implementation to match existing test expectations rather than breaking changes.

Applied to files:

  • apps/web/components/plain/__tests__/PlainContactForm.test.tsx
🔇 Additional comments (3)
apps/web/components/plain/__tests__/PlainContactForm.test.tsx (3)

61-67: LGTM: form visibility assertions are clear and robust


69-74: LGTM: initial state check reads well


163-175: LGTM: unauthenticated session path covered

Baseline behavior asserted; no issues.

Comment on lines 19 to +28
const mockFetch = vi.fn();
global.fetch = mockFetch;
global.fetch = mockFetch as unknown as typeof fetch;

describe("PlainContactForm", () => {
let mockSetIsOpen: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.clearAllMocks();
mockSetIsOpen = vi.fn();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Reset the fetch mock between tests to avoid leaked implementations

The “loading” test installs a never-resolving impl; since only vi.clearAllMocks() runs, that impl can bleed into later tests. Reset the mock each test.

Apply in beforeEach:

 beforeEach(() => {
   vi.clearAllMocks();
+  mockFetch.mockReset();
   mockSetIsOpen = vi.fn();

🏁 Script executed:

#!/bin/bash
sed -n '1,200p' apps/web/components/plain/__tests__/PlainContactForm.test.tsx

Length of output: 5663


Reset fetch mock implementation between tests

vi.clearAllMocks() only clears call history, not mock implementations—add mockFetch.mockReset() in each test’s setup to clear any custom implementations.

Apply in beforeEach:

 beforeEach(() => {
   vi.clearAllMocks();
+  mockFetch.mockReset();
   mockSetIsOpen = vi.fn();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mockFetch = vi.fn();
global.fetch = mockFetch;
global.fetch = mockFetch as unknown as typeof fetch;
describe("PlainContactForm", () => {
let mockSetIsOpen: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
mockSetIsOpen = vi.fn();
const mockFetch = vi.fn();
global.fetch = mockFetch as unknown as typeof fetch;
describe("PlainContactForm", () => {
let mockSetIsOpen: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
mockFetch.mockReset();
mockSetIsOpen = vi.fn();
});
// …
});
🤖 Prompt for AI Agents
In apps/web/components/plain/__tests__/PlainContactForm.test.tsx around lines 19
to 28, the test setup only calls vi.clearAllMocks() which clears call history
but does not reset mock implementations on global.fetch; add
mockFetch.mockReset() (or mockClear() as appropriate) inside the beforeEach
after vi.clearAllMocks() to reset any custom fetch implementations and ensure
each test starts with a clean mock implementation.

@anikdhabal anikdhabal merged commit 0a9e732 into main Sep 1, 2025
61 of 65 checks passed
@anikdhabal anikdhabal deleted the fix/plain_not_opening_free_users branch September 1, 2025 12:28
@github-actions
Copy link
Contributor

github-actions bot commented Sep 1, 2025

E2E results are ready!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug Something isn't working core area: core, team members only ready-for-e2e

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants