Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,128 @@ import { BlockNoteEditor } from "../../../editor/BlockNoteEditor.js";
import { handleFileInsertion } from "./handleFileInsertion.js";

describe("File insertion", () => {
describe.each(["success", "rejection", "empty URL"] as const)(
"%s for the first upload",
(outcome) => {
it.each(["before", "after", "paste"] as const)(
"preserves transfer order (%s a nonempty paragraph)",
async (placement) => {
let finishFirstUpload!: () => void;
let finishSecondUpload!: () => void;
const firstPending = new Promise<void>((resolve) => {
finishFirstUpload = resolve;
});
const secondPending = new Promise<void>((resolve) => {
finishSecondUpload = resolve;
});
const uploadFile = vi.fn(async (file: File) => {
if (file.name === "image.gif") {
await firstPending;
if (outcome === "rejection") {
throw new Error("Upload rejected");
}
if (outcome === "empty URL") {
return "";
}
} else {
await secondPending;
}
return `https://example.com/${file.name}`;
});
const editor = BlockNoteEditor.create({
initialContent: [
{ id: "target", type: "paragraph", content: "Wrapped paragraph" },
],
trailingBlock: false,
uploadFile,
});

try {
editor.setTextCursorPosition("target", "end");
// Only layout is substituted: resolve the real target node after each
// insertion, as posAtCoords would for a drop over the paragraph.
Object.defineProperty(editor, "prosemirrorView", {
value: {
posAtCoords: () => {
let targetPos = 0;
editor.prosemirrorState.doc.descendants((node, pos) => {
if (node.attrs.id === "target") {
targetPos = pos + 1;
}
});
return { pos: targetPos, inside: -1 };
},
} satisfies Pick<
BlockNoteEditor["prosemirrorView"],
"posAtCoords"
>,
});
Object.defineProperty(editor, "domElement", {
value: {
querySelector: () => ({
getBoundingClientRect: () => ({ top: 0, bottom: 100 }),
}),
},
});
const files = [
new File(["first"], "image.gif", { type: "image/gif" }),
new File(["second"], "picture.png", { type: "image/png" }),
];
const event =
placement === "paste"
? new ClipboardEvent("paste", { cancelable: true })
: new DragEvent("drop", { cancelable: true });
Object.defineProperties(event, {
[placement === "paste" ? "clipboardData" : "dataTransfer"]: {
value: {
types: ["Files"],
items: files.map((file) => ({
type: file.type,
getAsFile: () => file,
})),
},
},
clientX: { value: 10 },
clientY: { value: placement === "before" ? 10 : 90 },
});

const insertion = handleFileInsertion(event, editor);
// Uploads stay sequential. Let the first finish while the second is
// still pending, then check that its completion cannot reorder blocks.
expect(uploadFile).toHaveBeenCalledTimes(1);
finishFirstUpload();
await vi.waitFor(() => expect(uploadFile).toHaveBeenCalledTimes(2));

expect(event.defaultPrevented).toBe(true);
expect(uploadFile.mock.calls.map(([file]) => file)).toEqual(files);
const order = editor.document.map((block) =>
"name" in block.props ? block.props.name : block.id,
);
expect
.soft(order)
.toEqual(
placement === "before"
? ["image.gif", "picture.png", "target"]
: ["target", "image.gif", "picture.png"],
);
finishSecondUpload();
await insertion;
expect(
editor.document
.filter((block) => block.type === "image")
.map((block) => block.props.url),
).toEqual([
outcome === "success" ? "https://example.com/image.gif" : "",
"https://example.com/picture.png",
]);
} finally {
editor._tiptapEditor.destroy();
}
},
);
},
);

it.each(["drop", "paste"] as const)(
"uploads and inserts all files in order after %s data becomes unavailable",
async (eventType) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export async function handleFileInsertion<
type: item.type,
}));

let previousBlockId: string | undefined;

for (const { file, type } of files) {
// Gets file block corresponding to MIME type.
let fileBlockType = "file";
Expand Down Expand Up @@ -146,7 +148,18 @@ export async function handleFileInsertion<

let insertedBlockId: string | undefined = undefined;

if (event.type === "paste") {
const previousBlock = previousBlockId
? editor.getBlock(previousBlockId)
: undefined;
if (previousBlock) {
// Resolve the event's insertion point only for the first file. Reusing
// it for every file reverses insertions after a nonempty block.
insertedBlockId = editor.insertBlocks(
[fileBlock],
previousBlock,
"after",
)[0].id;
} else if (event.type === "paste") {
const currentBlock = editor.getTextCursorPosition().block;
insertedBlockId = insertOrUpdateBlock(editor, currentBlock, fileBlock);
} else if (event.type === "drop") {
Expand Down Expand Up @@ -195,6 +208,8 @@ export async function handleFileInsertion<
return;
}

previousBlockId = insertedBlockId;

const updateData = await editor
.uploadFile(file, insertedBlockId)
.catch(() => undefined);
Expand Down
Loading