mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-11 08:37:45 +00:00
Merge c21b35db70 into e2be9c9edd
This commit is contained in:
commit
7e462fc8b1
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 ||
|
pluginDocument?.summary ||
|
||||||
(_document?.memoryEntries && _document.memoryEntries.length > 0),
|
(_document?.memoryEntries && _document.memoryEntries.length > 0),
|
||||||
)
|
)
|
||||||
|
const deleteTargetKey = _document?.id
|
||||||
|
? `id:${_document.id}`
|
||||||
|
: _document?.customId
|
||||||
|
? `custom:${_document.customId}`
|
||||||
|
: "missing"
|
||||||
|
|
||||||
const documentPreview = (
|
const documentPreview = (
|
||||||
<div
|
<div
|
||||||
|
|
@ -352,6 +357,7 @@ export function DocumentModal({
|
||||||
<CopySessionIdButton sessionId={_document.customId} />
|
<CopySessionIdButton sessionId={_document.customId} />
|
||||||
)}
|
)}
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
|
key={deleteTargetKey}
|
||||||
documentId={_document?.id}
|
documentId={_document?.id}
|
||||||
customId={_document?.customId}
|
customId={_document?.customId}
|
||||||
deleteMutation={deleteMutation}
|
deleteMutation={deleteMutation}
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@
|
||||||
"@types/node": "^24.0.4",
|
"@types/node": "^24.0.4",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"happy-dom": "^20.9.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"wrangler": "^4.26.0"
|
"wrangler": "^4.26.0"
|
||||||
|
|
|
||||||
1
bun.lock
1
bun.lock
|
|
@ -251,6 +251,7 @@
|
||||||
"@types/node": "^24.0.4",
|
"@types/node": "^24.0.4",
|
||||||
"@types/react": "^19.2.9",
|
"@types/react": "^19.2.9",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"happy-dom": "^20.9.0",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"wrangler": "^4.26.0",
|
"wrangler": "^4.26.0",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue