mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(reader): preserve exact positions when syncing with koreader (#1694)
Convert complete source CFIs on the server to avoid chapter-start resets and browser-generated locators that disagree with CREngine. Preserve engine whitespace, Unicode offsets, element anchors and long text nodes. Handle prefixed OPF namespaces and retry outdated annotation conversions. Closes #1621 Closes #1288
This commit is contained in:
@@ -204,6 +204,59 @@ describe('KOReader progress position routing (e2e)', { timeout: 180_000 }, () =>
|
||||
expect(stored?.percentage).toBeCloseTo(80, 5);
|
||||
});
|
||||
|
||||
it.each(['web', 'narration'])('restores the complete %s reader position and retains its text offset on device upload', async (source) => {
|
||||
await ctx.db
|
||||
.update(schema.koreaderDeviceProgress)
|
||||
.set({ updatedAt: new Date('2020-01-01T00:00:00.000Z') })
|
||||
.where(eq(schema.koreaderDeviceProgress.bookFileId, epub.bookFileId));
|
||||
const cfi = 'epubcfi(/6/2!/4/2/1:3)';
|
||||
// Narration only advances a position; begin behind the position being restored.
|
||||
await saveFromWebReader(epub.bookFileId, { percentage: 10, cfi: 'epubcfi(/6/2!/4/2/1:0)' });
|
||||
await saveFromWebReader(epub.bookFileId, { percentage: 55, cfi, ...(source === 'narration' ? { source } : {}) });
|
||||
|
||||
const pull = await ctx.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/v1/koreader/syncs/progress/${epubHash}`,
|
||||
headers: deviceHeaders(),
|
||||
});
|
||||
expect(pull.statusCode).toBe(200);
|
||||
const remote = pull.json() as { percentage: number; progress: string; device: string };
|
||||
expect(remote).toEqual(expect.objectContaining({ percentage: 0.55, progress: '/body/DocFragment[1]/body/p/text().3', device: 'web' }));
|
||||
|
||||
// Readers paginate differently, but the text point must survive the lifecycle upload.
|
||||
await syncFromDevice(epubHash, 0.52, remote.progress);
|
||||
await expect(readerProgress(epub.bookFileId)).resolves.toEqual(expect.objectContaining({ cfi, percentage: 52 }));
|
||||
expect((await storedProgress(epub.bookFileId))?.koreaderProgress).toBe(remote.progress);
|
||||
});
|
||||
|
||||
it('preserves positions through repeated web and native-engine handoffs', async () => {
|
||||
for (let offset = 1; offset <= 4; offset += 1) {
|
||||
const cfi = `epubcfi(/6/2!/4/2/1:${offset})`;
|
||||
await saveFromWebReader(epub.bookFileId, { percentage: 20 + offset, cfi });
|
||||
const pull = await ctx.app.inject({ method: 'GET', url: `/api/v1/koreader/syncs/progress/${epubHash}`, headers: deviceHeaders() });
|
||||
expect(pull.statusCode).toBe(200);
|
||||
expect(pull.json().progress).toBe(`/body/DocFragment[1]/body/p/text().${offset}`);
|
||||
const nextOffset = offset + 1;
|
||||
await syncFromDevice(epubHash, (25 + offset) / 100, `/body[1]/DocFragment[1]/body[1]/p[1]/text()[1].${nextOffset}`);
|
||||
await expect(readerProgress(epub.bookFileId)).resolves.toEqual(expect.objectContaining({ cfi: `epubcfi(/6/2!/4/2/1:${nextOffset})` }));
|
||||
}
|
||||
});
|
||||
|
||||
it('serves percentage fallback for an unresolvable reader position', async () => {
|
||||
await ctx.db
|
||||
.update(schema.koreaderDeviceProgress)
|
||||
.set({ updatedAt: new Date('2020-01-01T00:00:00.000Z') })
|
||||
.where(eq(schema.koreaderDeviceProgress.bookFileId, epub.bookFileId));
|
||||
await saveFromWebReader(epub.bookFileId, { percentage: 30, cfi: 'epubcfi(/6/2!/4/999/1:3)' });
|
||||
const pull = await ctx.app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/v1/koreader/syncs/progress/${epubHash}`,
|
||||
headers: deviceHeaders(),
|
||||
});
|
||||
expect(pull.statusCode).toBe(200);
|
||||
expect(pull.json()).toEqual(expect.objectContaining({ progress: null, percentage: 0.3, device: 'web' }));
|
||||
});
|
||||
|
||||
it('never turns KOSync progress pushes into reading sessions', async () => {
|
||||
await syncFromDevice(epubHash, 0.4, XPOINTER);
|
||||
await ctx.db
|
||||
|
||||
Reference in New Issue
Block a user