| author | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-08 05:37:34 UTC |
| committer | Stephen Paul Weber
<singpolyma@singpolyma.net> 2026-04-08 05:43:28 UTC |
| parent | 2a9ca8d1b8e69c18c1895b07cb2a1b61925f77af |
| .gitignore | +7 | -0 |
| Makefile | +7 | -1 |
| borogove/persistence/IDB.js | +79 | -14 |
| playwright.config.ts | +52 | -0 |
| test/idb.spec.ts | +491 | -0 |
diff --git a/.gitignore b/.gitignore index 788f7c1..f10dcde 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,10 @@ haxedoc.xml cpp venv docs/js/borogove*.md + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/Makefile b/Makefile index b808b06..9348a1f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ HAXE_PATH=$$HOME/Software/haxe-4.3.1/hxnodejs/12,1,0/src -.PHONY: all test doc hx-build-dep cpp/libborogove.dso npm/borogove-browser.js npm/borogove.js cpp +.PHONY: all test doc hx-build-dep cpp/libborogove.dso npm/borogove-browser.js npm/borogove.js cpp playwright all: npm libborogove.batteriesincluded.so libborogove.so libborogove.a @@ -49,6 +49,12 @@ npm: npm/borogove-browser.js npm/borogove.js borogove/persistence/IDB.js borogov -cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums --allowJs --checkJs -d index.ts > /dev/null cd npm && npx tsc --esModuleInterop --lib esnext,dom --target esnext --preserveConstEnums --allowJs --checkJs -d index.ts +playwright/.cache/borogove.js: npm + esbuild npm/index.js --bundle --format=esm "--alias:node:dns=@xmpp/resolve" "--footer:js=export { borogove_JID as JID }" --outfile=$@ + +playwright: playwright/.cache/borogove.js + npx playwright test + cpp/libborogove.dso: haxe cpp.hxml $(RM) cpp/libborogove.dso.hash diff --git a/borogove/persistence/IDB.js b/borogove/persistence/IDB.js index 0670d77..90dd466 100644 --- a/borogove/persistence/IDB.js +++ b/borogove/persistence/IDB.js @@ -184,8 +184,8 @@ export default async (dbname, media, tokenize, stemmer) => { if (messagesIndexNames.contains("accounts")) { tx.objectStore("messages").deleteIndex("accounts"); } - if (messagesIndexNames.contains("chats")) { - tx.objectStore("messages").deleteIndex("chats"); + if (!messagesIndexNames.contains("chats")) { + tx.objectStore("messages").createIndex("chats", ["account", "chatId", "timestamp"]); } }; dbOpenReq.onsuccess = (event) => { @@ -209,7 +209,7 @@ export default async (dbname, media, tokenize, stemmer) => { } const tx = db.transaction(["messages", "keyvaluepairs"], "readonly"); const messagesIndexNames = tx.objectStore("messages").indexNames; - const wantIndexNames = ["chatsBySortId", "accountsBySortId", "terms"]; + const wantIndexNames = ["chatsBySortId", "accountsBySortId", "terms", "chats"]; for(const indexName of wantIndexNames) { if(!messagesIndexNames.contains(indexName)) { db.close(); @@ -230,7 +230,7 @@ export default async (dbname, media, tokenize, stemmer) => { await promisifyRequest(writeKV.objectStore("keyvaluepairs").put(new Date(), "__migrationAddSortIdAndTerms")); } - if (messagesIndexNames.contains("accounts") || messagesIndexNames.contains("chats")) { + if (messagesIndexNames.contains("accounts")) { db.close(); openDb(db.version + 1).then(resolve, reject); return; @@ -679,27 +679,92 @@ export default async (dbname, media, tokenize, stemmer) => { getMessagesBefore: async function(account, chatId, before) { const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = store.index("chatsBySortId").openCursor( - IDBKeyRange.bound([account, chatId], [account, chatId, before?.sortId || []]), - "prev" - ); - const messages = await this.getMessagesFromCursor(cursor, before); + const cursor = before?.type === enums.borogove_MessageType.MessageChannelPrivate ? + store.index("chats").openCursor( + IDBKeyRange.bound([account, chatId], [account, chatId, new Date(before.timestamp)]), + "prev" + ) : store.index("chatsBySortId").openCursor( + IDBKeyRange.bound([account, chatId], [account, chatId, before?.sortId || []]), + "prev" + ); + const messages = await this.getMessagesFromCursor(cursor, before, m => m.type === enums.borogove_MessageType.MessageChannelPrivate); + + if (messages.length > 0 && messages[0].serverIdBy === chatId) { + const earliest = new Date(messages[messages.length - 1].timestamp); + const tx = db.transaction(["messages"], "readonly"); + const store = tx.objectStore("messages"); + const pmCursor = store.index("chats").openCursor( + IDBKeyRange.bound([account, chatId], [account, chatId, before ? new Date(before.timestamp) : []]), + "prev" + ); + const promisePMs = []; + while (true) { + const cresult = await promisifyRequest(pmCursor); + if (!cresult?.value || cresult.value.timestamp < earliest) break; + + if (cresult.value.type === enums.borogove_MessageType.MessageChannelPrivate && (!before || before.serverId !== cresult.value.serverId)) { + promisePMs.push(hydrateMessage(cresult.value)); + } + + cresult.continue(); + } + + const pms = await Promise.all(promisePMs); + for (const pm of pms) { + const idx = messages.findIndex(m => m.timestamp <= pm.timestamp); + if (idx >= 0) messages.splice(idx, 0, pm); + } + } + return messages.reverse(); }, getMessagesAfter: async function(account, chatId, after) { - const bound = after?.sortId ? [after.sortId] : []; + const index = after?.type === enums.borogove_MessageType.MessageChannelPrivate ? "chats" : "chatsBySortId"; + const bound = after ? [after?.type === enums.borogove_MessageType.MessageChannelPrivate ? new Date(after.timestamp) : after.sortId] : []; const tx = db.transaction(["messages"], "readonly"); const store = tx.objectStore("messages"); - const cursor = store.index("chatsBySortId").openCursor( + const cursor = store.index(index).openCursor( IDBKeyRange.bound([account, chatId, ...bound], [account, chatId, []]), "next" ); - return this.getMessagesFromCursor(cursor, after); + const messages = await this.getMessagesFromCursor(cursor, after, m => m.type === enums.borogove_MessageType.MessageChannelPrivate); + + if (messages.length > 0 && messages[0].serverIdBy === chatId) { + const latest = new Date(messages[messages.length - 1].timestamp); + const tx = db.transaction(["messages"], "readonly"); + const store = tx.objectStore("messages"); + const pmCursor = store.index("chats").openCursor( + IDBKeyRange.bound([account, chatId, ...(after ? [new Date(after.timestamp)] : [])], [account, chatId, []]), + "next" + ); + const promisePMs = []; + while (true) { + const cresult = await promisifyRequest(pmCursor); + if (!cresult?.value) break; + + if (cresult.value.type === enums.borogove_MessageType.MessageChannelPrivate && (!after || after.serverId !== cresult.value.serverId)) { + promisePMs.push(hydrateMessage(cresult.value)); + } + + if (cresult.value.timestamp > latest) break; + + cresult.continue(); + } + + const pms = await Promise.all(promisePMs); + for (const pm of pms) { + const idx = messages.findLastIndex(m => m.timestamp < pm.timestamp); + if (idx >= 0) messages.splice(idx+1, 0, pm); + } + } + + return messages; }, getMessagesAround: async function(account, around) { if (!around) throw "Cannot look around nothing"; + if (around.type == enums.borogove_MessageType.MessageChannelPrivate) throw "Cannot look around PM"; const chatId = around.chatId(); const before = this.getMessagesBefore(account, chatId, around); @@ -714,13 +779,13 @@ export default async (dbname, media, tokenize, stemmer) => { return Promise.all([before, aroundAndAfter]).then(result => result.flat()); }, - getMessagesFromCursor: async function(cursor, notIncluding) { + getMessagesFromCursor: async function(cursor, notIncluding, filter) { const result = []; while (true) { const cresult = await promisifyRequest(cursor); if (cresult && result.length < 50) { const value = cresult.value; - if ((notIncluding?.serverId && notIncluding?.serverId === value?.serverId) || (notIncluding?.localId && !value?.serverId && notIncluding?.localId === value?.localId)) { + if ((notIncluding?.serverId && notIncluding?.serverId === value?.serverId) || (notIncluding?.localId && !value?.serverId && notIncluding?.localId === value?.localId) || (filter && filter(value))) { cresult.continue(); continue; } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..86965cb --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,52 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from "dotenv"; +// import path from "path"; +// dotenv.config({ path: path.resolve(__dirname, ".env") }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./test", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "dot", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto("")`. */ + // baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], +}); diff --git a/test/idb.spec.ts b/test/idb.spec.ts new file mode 100644 index 0000000..501994c --- /dev/null +++ b/test/idb.spec.ts @@ -0,0 +1,491 @@ +import { test, expect } from "@playwright/test"; +import fs from "fs"; + +test("1:1 come back ordered by sortId", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("hatter@example.com"); + builder.replyTo = [builder.from]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "alice@example.com", + senderId: "hatter@example.com", + direction: 0, + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("hatter@example.com"); + builder2.replyTo = [builder.from]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore("alice@example.com", "hatter@example.com"); + }, code); + + expect(result.length).toBe(2); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); +}); + +test("getMessagesBefore the end: MUC come back ordered by sortId, PM by timestamp", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a0"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore("alice@example.com", "teaparty@example.com"); + }, code); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); +}); + +test("getMessagesBefore some point: MUC come back ordered by sortId, PM by timestamp", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore("alice@example.com", "teaparty@example.com", builder4.build()); + }, code); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); +}); + +test("getMessagesBefore a PM", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesBefore("alice@example.com", "teaparty@example.com", builder3.build()); + }, code); + + expect(result.length).toBe(2); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); +}); + +test("getMessagesAfter the start: MUC come back ordered by sortId, PM by timestamp", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "a1"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter("alice@example.com", "teaparty@example.com"); + }, code); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("1"); + expect(result[1].serverId).toBe("2"); + expect(result[2].serverId).toBe("3"); +}); + +test("getMessagesAfter some point: MUC come back ordered by sortId, PM by timestamp", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter("alice@example.com", "teaparty@example.com", builder.build()); + }, code); + + expect(result.length).toBe(3); + expect(result[0].serverId).toBe("2"); + expect(result[1].serverId).toBe("3"); + expect(result[2].serverId).toBe("4"); +}); + +test("getMessagesAfter a PM", async ({ page }) => { + page.route("https://localhost/", route => route.fulfill({ body: "<html></html>" })); + const code = fs.readFileSync("playwright/.cache/borogove.js", "utf8"); + await page.goto("https://localhost/"); + const result = await page.evaluate(async (code) => { + const blob = new Blob([code], { type: 'text/javascript' }); + const borogove = await import(URL.createObjectURL(blob)); + + const mediaStore = borogove.persistence.MediaStoreCache("snikket"); + const persistence = await borogove.persistence.IDB("snikket", mediaStore); + + const builder = new borogove.ChatMessageBuilder({ + serverId: "1", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:00Z", + }); + builder.sortId = "a0"; + builder.to = borogove.JID.parse("alice@example.com"); + builder.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder.replyTo = [builder.from.asBare()]; + + const builder2 = new borogove.ChatMessageBuilder({ + serverId: "2", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:01Z", + }); + builder2.sortId = "b0"; + builder2.to = borogove.JID.parse("alice@example.com"); + builder2.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder2.replyTo = [builder.from.asBare()]; + + const builder3 = new borogove.ChatMessageBuilder({ + serverId: "3", + serverIdBy: "alice@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannelPrivate, + timestamp: "2020-01-01T00:00:03Z", + }); + builder3.sortId = "Z~"; + builder3.to = borogove.JID.parse("alice@example.com"); + builder3.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder3.replyTo = [builder.from.asBare()]; + + const builder4 = new borogove.ChatMessageBuilder({ + serverId: "4", + serverIdBy: "teaparty@example.com", + senderId: "teaparty@example.com/hatter", + direction: 0, + type: borogove.MessageType.MessageChannel, + timestamp: "2020-01-01T00:00:04Z", + }); + builder4.sortId = "c0"; + builder4.to = borogove.JID.parse("alice@example.com"); + builder4.from = borogove.JID.parse("teaparty@example.com/hatter"); + builder4.replyTo = [builder.from.asBare()]; + + await persistence.storeMessages("alice@example.com", [ + builder2.build(), + builder4.build(), + builder3.build(), + builder.build(), + ]); + + return await persistence.getMessagesAfter("alice@example.com", "teaparty@example.com", builder3.build()); + }, code); + + expect(result.length).toBe(1); + expect(result[0].serverId).toBe("4"); +});