forked from akai/readest
600d69fa50
* fix(reader): gate route View Transitions on the API, turns on groups (READEST-9) Reverts #4949, which opened books through the plain router to dodge the "Transition was aborted because of timeout in DOM update" TimeoutError (Sentry READEST-9). Rather than carve the transition out of one flow, gate it at the router: useAppRouter routes through the View Transition router only where the engine has the View Transitions API, and every into-reader path (including the reverted ones) goes back through useAppRouter. The base View Transitions API and nested view-transition groups reach very different browsers, so they become two separate appService capability flags, each backed by a probe in utils/viewTransition: * supportsViewTransitionsAPI (document.startViewTransition): the baseline a route crossfade needs, landing on Chrome 111+, Safari 18+, recent WebView. Gates the router. * supportsViewTransitionGroup (view-transition-group: nearest, Chrome/WebView 140+): the far narrower target the paginator's layered turns require. Gates the turn-style options and the captured-turn fallback. Both flags fold in the Linux WebKitGTK carve-out because it crashes on the snapshot, matching the supportsCanvasContext2DFilter precedent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tts): enlarge the Now Playing bar and scale its controls responsively Grow the collapsed bar to h-14 with a 10x10 cover and symmetric px-2 padding, drive the play/pause and close icons through useResponsiveSize instead of fixed pixel sizes, and cut the bottom safe-area contribution to a third so the bar sits closer to the screen edge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.9 KiB
TypeScript
99 lines
3.9 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
import { applyPageTurnAttributes, getCapturedTurnStyle } from '@/app/reader/hooks/useCapturedTurn';
|
|
import type { FoliateView } from '@/types/view';
|
|
import type { ViewSettings } from '@/types/book';
|
|
|
|
// The DOM lib types startViewTransition as always present; go through a
|
|
// loose shape so the stub can also remove it.
|
|
type VTDocument = { startViewTransition?: () => void };
|
|
|
|
// iOS 18 WebKit has startViewTransition but crashes the WebContent process on
|
|
// the layered turns (#555); engines with nested view-transition groups
|
|
// (Chrome/WebView 140+) are the ones known to run them reliably.
|
|
const stubEngine = ({
|
|
startViewTransition,
|
|
nestedGroups,
|
|
}: {
|
|
startViewTransition: boolean;
|
|
nestedGroups: boolean;
|
|
}) => {
|
|
const doc = document as unknown as VTDocument;
|
|
if (startViewTransition) doc.startViewTransition = () => {};
|
|
else delete doc.startViewTransition;
|
|
vi.stubGlobal('CSS', {
|
|
supports: (property: string, value: string) =>
|
|
nestedGroups && property === 'view-transition-group' && value === 'nearest',
|
|
});
|
|
};
|
|
|
|
const makeView = () => {
|
|
const renderer = document.createElement('foliate-paginator');
|
|
return { view: { renderer } as unknown as FoliateView, renderer };
|
|
};
|
|
|
|
const settings = (pageTurnStyle: ViewSettings['pageTurnStyle']) =>
|
|
({
|
|
pageTurnStyle,
|
|
animated: true,
|
|
scrolled: false,
|
|
disableSwipe: false,
|
|
isEink: false,
|
|
}) as ViewSettings;
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
delete (document as unknown as VTDocument).startViewTransition;
|
|
});
|
|
|
|
describe('getCapturedTurnStyle', () => {
|
|
it('captures the slide on Tauri when the engine cannot layer View Transitions', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'tauri');
|
|
stubEngine({ startViewTransition: true, nestedGroups: false });
|
|
expect(getCapturedTurnStyle(settings('slide'), false)).toBe('slide');
|
|
});
|
|
|
|
it('leaves the slide to View Transitions on fully supporting engines', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'tauri');
|
|
stubEngine({ startViewTransition: true, nestedGroups: true });
|
|
expect(getCapturedTurnStyle(settings('slide'), false)).toBeNull();
|
|
});
|
|
|
|
it('never captures outside Tauri platforms', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'web');
|
|
stubEngine({ startViewTransition: true, nestedGroups: false });
|
|
expect(getCapturedTurnStyle(settings('slide'), false)).toBeNull();
|
|
expect(getCapturedTurnStyle(settings('curl'), false)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('applyPageTurnAttributes', () => {
|
|
it('keeps the View Transition slide on fully supporting engines', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'web');
|
|
stubEngine({ startViewTransition: true, nestedGroups: true });
|
|
const { view, renderer } = makeView();
|
|
applyPageTurnAttributes(view, settings('slide'), false);
|
|
expect(renderer.getAttribute('turn-style')).toBe('slide');
|
|
});
|
|
|
|
it('falls back to push on web engines without full View Transitions support', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'web');
|
|
stubEngine({ startViewTransition: true, nestedGroups: false });
|
|
const { view, renderer } = makeView();
|
|
renderer.setAttribute('turn-style', 'slide');
|
|
applyPageTurnAttributes(view, settings('slide'), false);
|
|
expect(renderer.hasAttribute('turn-style')).toBe(false);
|
|
});
|
|
|
|
it('hands the slide to the capture pipeline on Tauri without full support', () => {
|
|
vi.stubEnv('NEXT_PUBLIC_APP_PLATFORM', 'tauri');
|
|
stubEngine({ startViewTransition: true, nestedGroups: false });
|
|
const { view, renderer } = makeView();
|
|
applyPageTurnAttributes(view, settings('slide'), false);
|
|
// The app slides the captured page itself: the paginator must not run
|
|
// its own View Transition nor its swipe tracking.
|
|
expect(renderer.hasAttribute('turn-style')).toBe(false);
|
|
expect(renderer.hasAttribute('no-swipe')).toBe(true);
|
|
});
|
|
});
|