99b9adfe85
* refactor(sync): extract provider-agnostic layout paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): extract wire envelope module Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): extract pure merge module with law tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): add FileSyncProvider and LocalStore interfaces Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): FileSyncEngine orchestration over a provider Port WebDAVSync's per-book + library-wide sync onto FileSyncProvider + LocalStore. Behavior preserved; the #4756 metadata-reconciliation test is retargeted to drive the engine through a fake provider + store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): move WebDAV client + connect settings under providers/webdav Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): WebDAVProvider implementing FileSyncProvider Wraps the WebDAV transport client, maps WebDAVRequestError to the neutral FileSyncError, and owns Tauri streaming upload/download. Adds a provider-conformance suite future backends can run against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): shared appService-backed LocalStore bridge Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reader): drive WebDAV sync through FileSyncEngine Construct a WebDAVProvider + shared LocalStore + engine once per hook; the inline buffered/streaming book-file loader collapses into the provider + store, so the hook no longer imports tauriUpload or the file path helpers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(settings): drive WebDAV library sync + browse through the provider WebDAVForm now builds a WebDAVProvider + shared LocalStore + engine and calls engine.syncLibrary; the ~170-line inline callback block (buffered/streaming loaders, URL+auth construction) is gone. WebDAVBrowsePane builds a provider for the engine-level deleteRemoteBookDir cleanup helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): remove WebDAV-specific sync module, WebDAV is now a provider Delete src/services/webdav (WebDAVSync/WebDAVPaths + the transitional client and connect-settings shims). The superseded webdav-metadata-sync test is replaced by engine-metadata-sync; webdav-delete now drives deleteRemoteBookDir through a WebDAVProvider and asserts FileSyncError. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(sync): hydrate library before WebDAV Sync now to prevent clobber Sync now while the library store was unloaded (app launched into reader/ settings without mounting the Library view) merged the engine's addBookToLibrary / updateBookMetadata against an empty in-memory library, persisting a downloaded book or a metadata update as the entire library and wiping what was on disk. Pre-existing bug surfaced during the file-sync review. Hydrate the store in handleSyncNow and harden the store bridge with a load-if-unloaded guard (mirrors useLibraryStore.updateBooks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): make listDirectory honor the FileSyncError contract listDirectory threw a plain Error (and let raw fetch failures escape), so WebDAVProvider flattened every list() failure to FileSyncError(UNKNOWN). Throw the same WebDAVRequestError taxonomy as the file-level helpers (AUTH_FAILED / NOT_FOUND / NETWORK) so the provider maps them correctly. Add list() cases to the provider-conformance suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(sync): cover streaming upload, discovery/download, and receive paths The metadata-sync gate only exercised the buffered metadata + config-merge paths. Add engine tests for streaming uploadStream (+ HEAD short-circuit + one-shot retry), remote-only discovery -> streaming download -> addBook, and the receive strategy (pull-only, no config or index writes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(sync): incremental WebDAV Sync now + bounded concurrency Sync now was a full walk of every book each run (675 round-trips even when nothing changed). Default to incremental: diff the local library against the shared library.json index per hash and only process books whose local copy is newer (or absent). book.updatedAt bumps on every progress/notes/metadata save (bookDataStore.saveConfig), so the index is a reliable per-book change marker. Remote-newer books pull their config in the reconcile pass so peer progress still propagates. A new 'Full Sync' toggle (default off) re-checks everything. Also run the reconcile / download / push phases over a bounded worker pool (default concurrency 4) instead of one book at a time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(sync): simplify Sync now toast to a single book count The completion toast built a multi-line success bullet list (downloaded / pulled / pushed / uploaded). Replace it with the same single-line info toast the native cloud sync uses: '{{count}} book(s) synced'. Add a booksSynced counter to the engine result (a Set of distinct hashes touched in any direction, since the per-action counters overlap under Full Sync). Failures still surface as a warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): raise toasts above modals so they aren't hidden by open dialogs Toasts rendered at z-50, below the Settings dialog (z-110) and ModalPortal (z-120), so a toast dispatched from an open dialog (e.g. WebDAV 'Sync now') was buried. The documented overlay scale already places toast at 130; the component just hadn't followed it. Move the toast to z-[130] and extend the zIndexScale invariant test to guard TOAST > MODAL/SETTINGS and APP_LOCK > TOAST. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
85 lines
3.3 KiB
TypeScript
85 lines
3.3 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
/**
|
|
* Coordinated overlay z-index scale.
|
|
*
|
|
* Body-portaled / full-screen overlays share a single global stacking order.
|
|
* They must all clear the desktop rounded-window page frame (`.window-border`,
|
|
* z-index 99 in globals.css) and then layer in this order (low -> high):
|
|
*
|
|
* 100 RSVP immersive reading overlay
|
|
* 101 RSVP immersive controls (start dialog / hint chip)
|
|
* 110 Settings app dialog (raised above RSVP for dictionary management)
|
|
* 120 modal / command-palette layer (ModalPortal, CommandPalette)
|
|
* 130 toast / alert
|
|
* 200 security lock screen (AppLockScreen)
|
|
*
|
|
* This test reads the values straight from source so a future change that
|
|
* re-orders the layers fails loudly. It encodes the regression that buried the
|
|
* "Add OPDS Catalog" dialog behind Settings on mobile: Settings had been raised
|
|
* to z-[10050] (PR #3235) above the ModalPortal at z-[100], so any modal opened
|
|
* from inside Settings rendered behind it.
|
|
*/
|
|
|
|
const PAGE_FRAME = 99; // .window-border in src/styles/globals.css
|
|
|
|
const read = (rel: string) => fs.readFileSync(path.join(process.cwd(), rel), 'utf8');
|
|
const firstZ = (src: string, re: RegExp): number => {
|
|
const m = src.match(re);
|
|
expect(m, `expected to find z-index via ${re} in source`).not.toBeNull();
|
|
return Number(m![1]);
|
|
};
|
|
|
|
const MODAL = firstZ(read('src/components/ModalPortal.tsx'), /z-\[(\d+)\]/);
|
|
const SETTINGS = firstZ(read('src/components/settings/SettingsDialog.tsx'), /!z-\[(\d+)\]/);
|
|
const RSVP_OVERLAY = firstZ(
|
|
read('src/app/reader/components/rsvp/RSVPOverlay.tsx'),
|
|
/fixed inset-0 z-\[(\d+)\] flex select-none/,
|
|
);
|
|
const RSVP_CONTROLS = firstZ(
|
|
read('src/app/reader/components/rsvp/RSVPStartDialog.tsx'),
|
|
/z-\[(\d+)\]/,
|
|
);
|
|
const TOAST = firstZ(read('src/components/Toast.tsx'), /toast z-\[(\d+)\]/);
|
|
const APP_LOCK = firstZ(read('src/components/AppLockScreen.tsx'), /z-\[(\d+)\]/);
|
|
|
|
describe('overlay z-index scale', () => {
|
|
it('renders a modal (e.g. Add OPDS Catalog) above the Settings dialog', () => {
|
|
// Regression: ModalPortal opened from inside Settings was buried (#add-catalog).
|
|
expect(MODAL).toBeGreaterThan(SETTINGS);
|
|
});
|
|
|
|
it('raises the Settings dialog above the RSVP immersive overlay', () => {
|
|
expect(SETTINGS).toBeGreaterThan(RSVP_OVERLAY);
|
|
});
|
|
|
|
it('keeps RSVP controls above the RSVP overlay', () => {
|
|
expect(RSVP_CONTROLS).toBeGreaterThan(RSVP_OVERLAY);
|
|
});
|
|
|
|
it('keeps the RSVP overlay above the desktop window-border page frame', () => {
|
|
expect(RSVP_OVERLAY).toBeGreaterThan(PAGE_FRAME);
|
|
});
|
|
|
|
it('raises toasts above every modal so they show over open dialogs', () => {
|
|
// Regression: a sync-complete toast dispatched from the open Settings
|
|
// dialog was buried because the toast sat at z-50, below Settings (110)
|
|
// and ModalPortal (120).
|
|
expect(TOAST).toBeGreaterThan(MODAL);
|
|
expect(TOAST).toBeGreaterThan(SETTINGS);
|
|
});
|
|
|
|
it('keeps the security lock screen on top of every modal and toast', () => {
|
|
expect(APP_LOCK).toBeGreaterThan(MODAL);
|
|
expect(APP_LOCK).toBeGreaterThan(TOAST);
|
|
});
|
|
|
|
it('uses a compact scale with no four-digit z-index', () => {
|
|
for (const value of [RSVP_OVERLAY, RSVP_CONTROLS, SETTINGS, MODAL, TOAST, APP_LOCK]) {
|
|
expect(value).toBeLessThan(1000);
|
|
}
|
|
});
|
|
});
|