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
87 changes: 87 additions & 0 deletions src/forms/components/FormFolderSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from "react"
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"

const saveMeta = vi.fn()
const createFolderFn = vi.fn()
const toastError = vi.fn()
let folders = [
{ id: 1, name: "Surveys" },
{ id: 2, name: "Experiments" },
]
const refetch = vi.fn(async () => undefined)

vi.mock("@blitzjs/rpc", () => ({
useQuery: () => [folders, { refetch }],
// the first useMutation in the component is updateFormMeta, the second createFolder
useMutation: (mutation: { name?: string }) => [
(...args: unknown[]) =>
mutation.name === "createFolder" ? createFolderFn(...args) : saveMeta(...args),
],
}))
vi.mock("src/folders/queries/getFolders", () => ({ default: {} }))
vi.mock("src/folders/mutations/createFolder", () => ({ default: { name: "createFolder" } }))
vi.mock("src/forms/mutations/updateFormMeta", () => ({ default: { name: "updateFormMeta" } }))
vi.mock("react-hot-toast", () => ({ default: { error: (...a: unknown[]) => toastError(...a) } }))

import FormFolderSelector from "./FormFolderSelector"

const dropdown = () => screen.getByRole("combobox") as HTMLSelectElement

describe("FormFolderSelector", () => {
beforeEach(() => {
vi.clearAllMocks()
saveMeta.mockResolvedValue({})
})
afterEach(cleanup)

test("shows the folder the form is in", () => {
render(<FormFolderSelector formId={5} currentFolderId={2} />)
expect(dropdown().value).toBe("2")
})

test("keeps showing the folder you chose, even though the page never passes the new one back", async () => {
render(<FormFolderSelector formId={5} currentFolderId={null} />)
fireEvent.change(dropdown(), { target: { value: "2" } })

await waitFor(() => expect(saveMeta).toHaveBeenCalledWith({ id: 5, folderId: 2 }))
expect(dropdown().value).toBe("2")
})

test("choosing No folder is shown and saved as no folder", async () => {
render(<FormFolderSelector formId={5} currentFolderId={1} />)
fireEvent.change(dropdown(), { target: { value: "" } })

await waitFor(() => expect(saveMeta).toHaveBeenCalledWith({ id: 5, folderId: null }))
expect(dropdown().value).toBe("")
})

test("goes back to the previous folder and says so if saving fails", async () => {
saveMeta.mockRejectedValue(new Error("offline"))
render(<FormFolderSelector formId={5} currentFolderId={1} />)
fireEvent.change(dropdown(), { target: { value: "2" } })

await waitFor(() => expect(toastError).toHaveBeenCalled())
expect(dropdown().value).toBe("1")
})

test("follows the page if it does pass a different folder later", () => {
const { rerender } = render(<FormFolderSelector formId={5} currentFolderId={1} />)
rerender(<FormFolderSelector formId={5} currentFolderId={2} />)
expect(dropdown().value).toBe("2")
})

test("a newly created folder is selected", async () => {
createFolderFn.mockImplementation(async () => {
folders = [...folders, { id: 3, name: "New one" }]
return { id: 3, name: "New one" }
})
render(<FormFolderSelector formId={5} currentFolderId={null} />)
fireEvent.click(screen.getByText("+ New folder"))
fireEvent.change(screen.getByPlaceholderText("Folder name"), { target: { value: "New one" } })
fireEvent.click(screen.getByText("Create"))

await waitFor(() => expect(saveMeta).toHaveBeenCalledWith({ id: 5, folderId: 3 }))
await waitFor(() => expect(dropdown().value).toBe("3"))
})
})
26 changes: 22 additions & 4 deletions src/forms/components/FormFolderSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState } from "react"
import { useEffect, useState } from "react"
import { useQuery, useMutation } from "@blitzjs/rpc"
import toast from "react-hot-toast"
import getFolders from "src/folders/queries/getFolders"
import createFolder from "src/folders/mutations/createFolder"
import updateFormMeta from "src/forms/mutations/updateFormMeta"
Expand All @@ -16,18 +17,35 @@ export default function FormFolderSelector({ formId, currentFolderId, onUpdate }
const [createFolderMutation] = useMutation(createFolder)
const [newFolderName, setNewFolderName] = useState("")
const [showNewFolder, setShowNewFolder] = useState(false)
// The dropdown shows its own selection. The page only tells us the folder the form had when it
// loaded and doesn't pass it back after a change, so reading the value from the prop alone
// snapped the dropdown back to the old folder even though the change had been saved.
const [selectedFolderId, setSelectedFolderId] = useState<number | null>(currentFolderId)

useEffect(() => {
setSelectedFolderId(currentFolderId)
}, [currentFolderId])

const handleChange = async (e: React.ChangeEvent<HTMLSelectElement>) => {
const val = e.target.value
const folderId = val === "" ? null : parseInt(val, 10)
await updateMeta({ id: formId, folderId })
onUpdate?.(folderId)
const previous = selectedFolderId
setSelectedFolderId(folderId)
try {
await updateMeta({ id: formId, folderId })
onUpdate?.(folderId)
} catch (error) {
// not saved, so don't pretend it was
setSelectedFolderId(previous)
toast.error("The folder could not be saved. Please try again.")
}
}

const handleCreateFolder = async () => {
if (!newFolderName.trim()) return
const folder = await createFolderMutation({ name: newFolderName.trim() })
await updateMeta({ id: formId, folderId: folder.id })
setSelectedFolderId(folder.id)
setNewFolderName("")
setShowNewFolder(false)
await refetch()
Expand All @@ -38,7 +56,7 @@ export default function FormFolderSelector({ formId, currentFolderId, onUpdate }
<div className="flex flex-col gap-2">
<select
className="select text-base text-primary select-primary select-bordered border-2 bg-base-300 w-1/2 focus:outline-secondary focus:outline-offset-0 focus:outline-width-3"
value={currentFolderId ?? ""}
value={selectedFolderId ?? ""}
onChange={handleChange}
>
<option value="">No folder</option>
Expand Down
Loading