07f4c27d58
The browser modal allows users to navigate their reMarkable file structure, view documents and folders, and perform actions on individual files. Users can either import documents directly or convert handwriting to Markdown during import. The implementation includes: - New ribbon icon and command to open the browser - Modal UI with navigation controls (up/refresh) - Document listing with folder traversal - Per-file actions: Import and Handwriting to Markdown - Proper error handling and user feedback - Comprehensive tests for the new functionality The downloader was also enhanced to support the new conversion options through a more flexible interface.
173 lines
5.2 KiB
TypeScript
173 lines
5.2 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp } from "fs/promises";
|
|
import { tmpdir } from "os";
|
|
import { join } from "path";
|
|
import { DocumentDownloader } from "../src/sync/downloader";
|
|
import { RmapiNode } from "../src/types";
|
|
|
|
function createDocument(overrides: Partial<RmapiNode> = {}): RmapiNode {
|
|
return {
|
|
id: "doc-1",
|
|
name: "Note?1",
|
|
type: "DocumentType",
|
|
version: 7,
|
|
modifiedClient: "2026-05-31T10:00:00Z",
|
|
currentPage: 0,
|
|
parent: "",
|
|
tags: [],
|
|
starred: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function createPluginFixture(options: { synced?: any } = {}) {
|
|
const basePath = await mkdtemp(join(tmpdir(), "remarkable-plugin-test-"));
|
|
const calls: any = {
|
|
mkdir: [],
|
|
downloadFile: [],
|
|
downloadAnnotatedPdf: [],
|
|
processDocument: [],
|
|
trackDocument: [],
|
|
};
|
|
|
|
const plugin: any = {
|
|
settings: {
|
|
downloadPath: "remarkable",
|
|
convertToPdf: true,
|
|
enableHandwritingMd: true,
|
|
},
|
|
app: {
|
|
vault: {
|
|
adapter: {
|
|
getBasePath: () => basePath,
|
|
mkdir: async (path: string) => {
|
|
calls.mkdir.push(path);
|
|
},
|
|
exists: async () => true,
|
|
},
|
|
},
|
|
},
|
|
tracker: {
|
|
getSyncedDocument: async () => options.synced ?? null,
|
|
trackDocument: async (...args: any[]) => {
|
|
calls.trackDocument.push(args);
|
|
},
|
|
},
|
|
rmapi: {
|
|
list: async () => [],
|
|
downloadFile: async (...args: any[]) => {
|
|
calls.downloadFile.push(args);
|
|
},
|
|
downloadAnnotatedPdf: async (...args: any[]) => {
|
|
calls.downloadAnnotatedPdf.push(args);
|
|
},
|
|
},
|
|
ocrPipeline: {
|
|
processDocument: async (...args: any[]) => {
|
|
calls.processDocument.push(args);
|
|
return args[1];
|
|
},
|
|
},
|
|
};
|
|
|
|
return { basePath, calls, plugin };
|
|
}
|
|
|
|
test("downloadDocument writes nested remote documents to sanitized vault paths", async () => {
|
|
const { basePath, calls, plugin } = await createPluginFixture();
|
|
const downloader = new DocumentDownloader(plugin);
|
|
const doc = createDocument();
|
|
|
|
await downloader.downloadDocument({
|
|
node: doc,
|
|
remotePath: "/Folder A/Sub:Folder/Note?1",
|
|
});
|
|
|
|
assert.deepEqual(calls.mkdir, ["remarkable", "remarkable/Folder A", "remarkable/Folder A/Sub_Folder"]);
|
|
assert.deepEqual(calls.downloadFile, [
|
|
["/Folder A/Sub:Folder/Note?1", join(basePath, "remarkable", "Folder A", "Sub_Folder", "Note_1.rm")],
|
|
]);
|
|
assert.deepEqual(calls.downloadAnnotatedPdf, [
|
|
["/Folder A/Sub:Folder/Note?1", join(basePath, "remarkable", "Folder A", "Sub_Folder", "Note_1.pdf")],
|
|
]);
|
|
assert.deepEqual(calls.processDocument, [
|
|
["remarkable/Folder A/Sub_Folder/Note_1.rm", "remarkable/Folder A/Sub_Folder/Note_1.md"],
|
|
]);
|
|
assert.equal(calls.trackDocument.length, 2);
|
|
assert.deepEqual(calls.trackDocument[1], [doc, "remarkable/Folder A/Sub_Folder/Note_1.rm", true, true]);
|
|
});
|
|
|
|
test("downloadDocument skips documents with matching remote version and modified timestamp", async () => {
|
|
const doc = createDocument();
|
|
const { calls, plugin } = await createPluginFixture({
|
|
synced: {
|
|
id: doc.id,
|
|
remoteVersion: doc.version,
|
|
remoteModified: doc.modifiedClient,
|
|
hasMd: true,
|
|
},
|
|
});
|
|
const downloader = new DocumentDownloader(plugin);
|
|
|
|
await downloader.downloadDocument({
|
|
node: doc,
|
|
remotePath: "/Folder A/Note?1",
|
|
});
|
|
|
|
assert.deepEqual(calls.downloadFile, []);
|
|
assert.deepEqual(calls.trackDocument, []);
|
|
});
|
|
|
|
test("downloadDocument import action can skip handwriting OCR", async () => {
|
|
const { calls, plugin } = await createPluginFixture();
|
|
plugin.settings.enableHandwritingMd = true;
|
|
const downloader = new DocumentDownloader(plugin);
|
|
|
|
await downloader.downloadDocument(
|
|
{
|
|
node: createDocument(),
|
|
remotePath: "/Folder A/Note?1",
|
|
},
|
|
{ convertToMd: false },
|
|
);
|
|
|
|
assert.equal(calls.downloadFile.length, 1);
|
|
assert.deepEqual(calls.processDocument, []);
|
|
assert.deepEqual(calls.trackDocument.at(-1), [createDocument(), "remarkable/Folder A/Note_1.rm", true, false]);
|
|
});
|
|
|
|
test("downloadDocument markdown action runs OCR even when automatic OCR setting is disabled", async () => {
|
|
const { calls, plugin } = await createPluginFixture();
|
|
plugin.settings.enableHandwritingMd = false;
|
|
const downloader = new DocumentDownloader(plugin);
|
|
|
|
await downloader.downloadDocument(
|
|
{
|
|
node: createDocument(),
|
|
remotePath: "/Folder A/Note?1",
|
|
},
|
|
{ convertToMd: true },
|
|
);
|
|
|
|
assert.deepEqual(calls.processDocument, [["remarkable/Folder A/Note_1.rm", "remarkable/Folder A/Note_1.md"]]);
|
|
assert.deepEqual(calls.trackDocument.at(-1), [createDocument(), "remarkable/Folder A/Note_1.rm", true, true]);
|
|
});
|
|
|
|
test("syncAll propagates download failures after showing a failure notice", async () => {
|
|
const { plugin } = await createPluginFixture();
|
|
const downloader = new DocumentDownloader(plugin);
|
|
const originalError = console.error;
|
|
plugin.rmapi.list = async () => [createDocument()];
|
|
plugin.rmapi.downloadFile = async () => {
|
|
throw new Error("rmapi failed");
|
|
};
|
|
|
|
console.error = () => {};
|
|
try {
|
|
await assert.rejects(() => downloader.syncAll(), /rmapi failed/);
|
|
} finally {
|
|
console.error = originalError;
|
|
}
|
|
});
|