mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-07 02:58:11 +00:00
fix(web): reset delete confirmation when document changes
This commit is contained in:
parent
e651045ac5
commit
c21b35db70
4 changed files with 205 additions and 0 deletions
197
apps/web/components/document-modal/index.test.tsx
Normal file
197
apps/web/components/document-modal/index.test.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
import { Window } from "happy-dom"
|
||||
import { type ComponentProps, type ReactNode, act } from "react"
|
||||
import { createRoot, type Root } from "react-dom/client"
|
||||
|
||||
const browser = new Window({ url: "http://localhost/" })
|
||||
|
||||
Object.assign(globalThis, {
|
||||
window: browser,
|
||||
document: browser.document,
|
||||
navigator: browser.navigator,
|
||||
location: browser.location,
|
||||
history: browser.history,
|
||||
localStorage: browser.localStorage,
|
||||
sessionStorage: browser.sessionStorage,
|
||||
HTMLElement: browser.HTMLElement,
|
||||
Element: browser.Element,
|
||||
Node: browser.Node,
|
||||
MutationObserver: browser.MutationObserver,
|
||||
getComputedStyle: browser.getComputedStyle.bind(browser),
|
||||
requestAnimationFrame: browser.requestAnimationFrame.bind(browser),
|
||||
cancelAnimationFrame: browser.cancelAnimationFrame.bind(browser),
|
||||
})
|
||||
Object.assign(browser, { SyntaxError })
|
||||
;(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT: boolean
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
function PassThrough({ children }: { children?: ReactNode }) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
type MotionProps = {
|
||||
initial?: unknown
|
||||
animate?: unknown
|
||||
exit?: unknown
|
||||
transition?: unknown
|
||||
}
|
||||
|
||||
function MotionButton({
|
||||
initial: _initial,
|
||||
animate: _animate,
|
||||
exit: _exit,
|
||||
transition: _transition,
|
||||
...props
|
||||
}: ComponentProps<"button"> & MotionProps) {
|
||||
return <button {...props} />
|
||||
}
|
||||
|
||||
function MotionDiv({
|
||||
initial: _initial,
|
||||
animate: _animate,
|
||||
exit: _exit,
|
||||
transition: _transition,
|
||||
...props
|
||||
}: ComponentProps<"div"> & MotionProps) {
|
||||
return <div {...props} />
|
||||
}
|
||||
|
||||
// Keep DocumentModal and its private DeleteButton real; only remove unrelated UI.
|
||||
mock.module(import.meta.resolve("./title"), () => ({ Title: PassThrough }))
|
||||
mock.module(import.meta.resolve("./summary"), () => ({ Summary: PassThrough }))
|
||||
mock.module(import.meta.resolve("./graph-list-memories"), () => ({
|
||||
GraphListMemories: PassThrough,
|
||||
}))
|
||||
mock.module(import.meta.resolve("./content"), () => ({
|
||||
DocumentContent: PassThrough,
|
||||
}))
|
||||
|
||||
mock.module("@repo/ui/components/dialog", () => ({
|
||||
Dialog: PassThrough,
|
||||
DialogContent: PassThrough,
|
||||
DialogTitle: PassThrough,
|
||||
}))
|
||||
mock.module("@repo/ui/components/drawer", () => ({
|
||||
Drawer: PassThrough,
|
||||
DrawerContent: PassThrough,
|
||||
DrawerTitle: PassThrough,
|
||||
}))
|
||||
mock.module("@ui/components/tabs", () => ({
|
||||
Tabs: PassThrough,
|
||||
TabsContent: PassThrough,
|
||||
TabsList: PassThrough,
|
||||
TabsTrigger: PassThrough,
|
||||
}))
|
||||
mock.module("@radix-ui/react-dialog", () => ({ Close: PassThrough }))
|
||||
mock.module("motion/react", () => ({
|
||||
AnimatePresence: PassThrough,
|
||||
motion: { button: MotionButton, div: MotionDiv },
|
||||
}))
|
||||
mock.module("@hooks/use-mobile", () => ({ useIsMobile: () => false }))
|
||||
mock.module("@/lib/fonts", () => ({ dmSansClassName: () => "" }))
|
||||
mock.module("@/lib/plugin-document", () => ({
|
||||
parsePluginDocument: () => null,
|
||||
}))
|
||||
mock.module("@/hooks/use-full-document", () => ({
|
||||
useFullDocumentContent: () => null,
|
||||
}))
|
||||
mock.module("sonner", () => ({
|
||||
toast: { error: mock(() => {}), success: mock(() => {}) },
|
||||
}))
|
||||
|
||||
const deleteMutate = mock((_variables: { documentId: string }) => {})
|
||||
const deleteMutation = { isPending: false, mutate: deleteMutate }
|
||||
const updateMutation = { isPending: false, mutate: mock(() => {}) }
|
||||
|
||||
mock.module("@/hooks/use-document-mutations", () => ({
|
||||
useDocumentMutations: () => ({ deleteMutation, updateMutation }),
|
||||
}))
|
||||
|
||||
const { DocumentModal } = await import("./index")
|
||||
|
||||
type ModalDocument = NonNullable<
|
||||
ComponentProps<typeof DocumentModal>["document"]
|
||||
>
|
||||
|
||||
function makeDocument(
|
||||
id: string | null,
|
||||
customId: string | null,
|
||||
): ModalDocument {
|
||||
return {
|
||||
id,
|
||||
customId,
|
||||
title: id ?? customId ?? "Document",
|
||||
type: "unknown",
|
||||
url: null,
|
||||
content: "",
|
||||
summary: null,
|
||||
memoryEntries: [],
|
||||
createdAt: new Date(),
|
||||
} as unknown as ModalDocument
|
||||
}
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
const onClose = mock(() => {})
|
||||
|
||||
beforeEach(() => {
|
||||
deleteMutate.mockClear()
|
||||
onClose.mockClear()
|
||||
container = document.createElement("div")
|
||||
document.body.append(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => root.unmount())
|
||||
container.remove()
|
||||
})
|
||||
|
||||
async function renderDocument(value: ModalDocument) {
|
||||
await act(async () => {
|
||||
root.render(<DocumentModal document={value} isOpen onClose={onClose} />)
|
||||
})
|
||||
}
|
||||
|
||||
function findButton(label: string) {
|
||||
return Array.from(document.getElementsByTagName("button")).find((button) =>
|
||||
button.textContent?.includes(label),
|
||||
)
|
||||
}
|
||||
|
||||
async function clickButton(label: string) {
|
||||
const button = findButton(label)
|
||||
if (!button) throw new Error(`Missing button: ${label}`)
|
||||
|
||||
await act(async () => button.click())
|
||||
}
|
||||
|
||||
describe("DocumentModal delete confirmation", () => {
|
||||
it("disarms confirmation whenever the delete target changes", async () => {
|
||||
await renderDocument(makeDocument("id-a", null))
|
||||
await clickButton("Delete document")
|
||||
|
||||
await renderDocument(makeDocument("id-b", null))
|
||||
expect(findButton("Confirm delete")).toBeUndefined()
|
||||
|
||||
await clickButton("Delete document")
|
||||
await renderDocument(makeDocument(null, "custom-b"))
|
||||
expect(findButton("Confirm delete")).toBeUndefined()
|
||||
|
||||
await clickButton("Delete document")
|
||||
await renderDocument(makeDocument(null, "custom-c"))
|
||||
expect(findButton("Confirm delete")).toBeUndefined()
|
||||
expect(deleteMutate).not.toHaveBeenCalled()
|
||||
|
||||
await clickButton("Delete document")
|
||||
await clickButton("Confirm delete")
|
||||
|
||||
expect(deleteMutate).toHaveBeenCalledTimes(1)
|
||||
expect(deleteMutate).toHaveBeenCalledWith({
|
||||
documentId: "custom-c",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -284,6 +284,11 @@ export function DocumentModal({
|
|||
pluginDocument?.summary ||
|
||||
(_document?.memoryEntries && _document.memoryEntries.length > 0),
|
||||
)
|
||||
const deleteTargetKey = _document?.id
|
||||
? `id:${_document.id}`
|
||||
: _document?.customId
|
||||
? `custom:${_document.customId}`
|
||||
: "missing"
|
||||
|
||||
const documentPreview = (
|
||||
<div
|
||||
|
|
@ -352,6 +357,7 @@ export function DocumentModal({
|
|||
<CopySessionIdButton sessionId={_document.customId} />
|
||||
)}
|
||||
<DeleteButton
|
||||
key={deleteTargetKey}
|
||||
documentId={_document?.id}
|
||||
customId={_document?.customId}
|
||||
deleteMutation={deleteMutation}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@
|
|||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"happy-dom": "^20.9.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0"
|
||||
|
|
|
|||
1
bun.lock
1
bun.lock
|
|
@ -251,6 +251,7 @@
|
|||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"happy-dom": "^20.9.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.26.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue