refactor(app): organize agent stream rendering
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__private__,
|
||||
deriveBottomAnchorBlockedReason,
|
||||
type BottomAnchorMode,
|
||||
} from "./bottom-anchor-controller";
|
||||
import type { BottomAnchorTransportBehavior } from "./strategy";
|
||||
|
||||
type MeasurementState = ReturnType<typeof createMeasurementState>;
|
||||
|
||||
function createMeasurementState(
|
||||
overrides?: Partial<{
|
||||
containerKey: string;
|
||||
viewportWidth: number;
|
||||
viewportHeight: number;
|
||||
contentHeight: number;
|
||||
offsetY: number;
|
||||
viewportMeasuredForKey: string | null;
|
||||
contentMeasuredForKey: string | null;
|
||||
}>,
|
||||
) {
|
||||
return {
|
||||
containerKey: "scroll-view",
|
||||
viewportWidth: 0,
|
||||
viewportHeight: 0,
|
||||
contentHeight: 0,
|
||||
offsetY: 0,
|
||||
viewportMeasuredForKey: null,
|
||||
contentMeasuredForKey: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createPendingRequest() {
|
||||
return {
|
||||
id: 1,
|
||||
agentId: "agent-1",
|
||||
reason: "initial-entry" as const,
|
||||
requestKey: "route:agent-1",
|
||||
};
|
||||
}
|
||||
|
||||
function createFrameScheduler() {
|
||||
let sequence = 0;
|
||||
const tasks = new Map<
|
||||
number,
|
||||
{
|
||||
cancelled: boolean;
|
||||
remainingFrames: number;
|
||||
callback: () => void;
|
||||
kind: "attempt" | "verification";
|
||||
}
|
||||
>();
|
||||
|
||||
return {
|
||||
schedule(params: {
|
||||
kind: "attempt" | "verification";
|
||||
callback: () => void;
|
||||
delayFrames?: number;
|
||||
}) {
|
||||
const id = ++sequence;
|
||||
tasks.set(id, {
|
||||
cancelled: false,
|
||||
remainingFrames: Math.max(0, params.delayFrames ?? 0),
|
||||
callback: params.callback,
|
||||
kind: params.kind,
|
||||
});
|
||||
return id;
|
||||
},
|
||||
cancel(handle: unknown) {
|
||||
const task = tasks.get(handle as number);
|
||||
if (task) {
|
||||
task.cancelled = true;
|
||||
}
|
||||
},
|
||||
flushFrame() {
|
||||
const due: Array<() => void> = [];
|
||||
for (const [id, task] of Array.from(tasks.entries())) {
|
||||
if (task.cancelled) {
|
||||
tasks.delete(id);
|
||||
continue;
|
||||
}
|
||||
if (task.remainingFrames > 0) {
|
||||
task.remainingFrames -= 1;
|
||||
continue;
|
||||
}
|
||||
tasks.delete(id);
|
||||
due.push(task.callback);
|
||||
}
|
||||
for (const callback of due) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
flushAll(limit = 20) {
|
||||
for (let index = 0; index < limit && tasks.size > 0; index += 1) {
|
||||
this.flushFrame();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createDriverHarness(input?: {
|
||||
transportBehavior?: BottomAnchorTransportBehavior;
|
||||
isNearBottom?: boolean;
|
||||
measurementState?: MeasurementState;
|
||||
authoritativeReady?: boolean;
|
||||
}) {
|
||||
const scheduler = createFrameScheduler();
|
||||
const measurementState =
|
||||
input?.measurementState ??
|
||||
createMeasurementState({
|
||||
viewportWidth: 800,
|
||||
viewportHeight: 480,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
});
|
||||
const context = {
|
||||
agentId: "agent-1",
|
||||
authoritativeReady: input?.authoritativeReady ?? true,
|
||||
renderStrategy: "forward-stream",
|
||||
transportBehavior: input?.transportBehavior ?? {
|
||||
verificationDelayFrames: 0,
|
||||
verificationRetryMode: "rescroll",
|
||||
},
|
||||
measurementState,
|
||||
nearBottom: input?.isNearBottom ?? true,
|
||||
};
|
||||
const scrollToBottom = vi.fn(() => {
|
||||
context.nearBottom = true;
|
||||
context.measurementState.offsetY = 720;
|
||||
});
|
||||
const modeChanges: BottomAnchorMode[] = [];
|
||||
const driver = __private__.createBottomAnchorControllerDriver({
|
||||
getAgentId: () => context.agentId,
|
||||
getIsAuthoritativeHistoryReady: () => context.authoritativeReady,
|
||||
getRenderStrategy: () => context.renderStrategy,
|
||||
getTransportBehavior: () => context.transportBehavior,
|
||||
getMeasurementState: () => context.measurementState,
|
||||
isNearBottom: () => context.nearBottom,
|
||||
scrollToBottom,
|
||||
onModeChange: (mode) => {
|
||||
modeChanges.push(mode);
|
||||
},
|
||||
scheduleFrame: (params) => scheduler.schedule(params),
|
||||
cancelFrame: (handle) => scheduler.cancel(handle),
|
||||
});
|
||||
|
||||
return {
|
||||
context,
|
||||
driver,
|
||||
scheduler,
|
||||
scrollToBottom,
|
||||
modeChanges,
|
||||
};
|
||||
}
|
||||
|
||||
describe("deriveBottomAnchorBlockedReason", () => {
|
||||
it("keeps initial-entry pending until history is ready and geometry is measurable", () => {
|
||||
const pendingRequest = createPendingRequest();
|
||||
|
||||
expect(
|
||||
deriveBottomAnchorBlockedReason({
|
||||
pendingRequest,
|
||||
isAuthoritativeHistoryReady: false,
|
||||
measurementState: createMeasurementState(),
|
||||
pendingVerificationRequestId: null,
|
||||
}),
|
||||
).toBe("waiting_for_history_readiness");
|
||||
|
||||
expect(
|
||||
deriveBottomAnchorBlockedReason({
|
||||
pendingRequest,
|
||||
isAuthoritativeHistoryReady: true,
|
||||
measurementState: createMeasurementState({
|
||||
viewportHeight: 480,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
pendingVerificationRequestId: null,
|
||||
}),
|
||||
).toBe("waiting_for_measurable_content");
|
||||
|
||||
expect(
|
||||
deriveBottomAnchorBlockedReason({
|
||||
pendingRequest,
|
||||
isAuthoritativeHistoryReady: true,
|
||||
measurementState: createMeasurementState({
|
||||
viewportHeight: 480,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
pendingVerificationRequestId: pendingRequest.id,
|
||||
}),
|
||||
).toBe("waiting_for_post_layout_verification");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bottom anchor controller driver", () => {
|
||||
it("keeps initial-entry pending until authoritative history and current geometry exist", () => {
|
||||
const harness = createDriverHarness({
|
||||
authoritativeReady: false,
|
||||
measurementState: createMeasurementState(),
|
||||
});
|
||||
|
||||
harness.driver.applyRouteRequest({
|
||||
agentId: "agent-1",
|
||||
reason: "initial-entry",
|
||||
requestKey: "route:agent-1:initial-entry",
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).not.toHaveBeenCalled();
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
mode: "sticky-bottom",
|
||||
blockedReason: "waiting_for_history_readiness",
|
||||
pendingRequest: {
|
||||
reason: "initial-entry",
|
||||
},
|
||||
});
|
||||
|
||||
harness.context.authoritativeReady = true;
|
||||
harness.context.measurementState.viewportHeight = 480;
|
||||
harness.context.measurementState.contentHeight = 1200;
|
||||
harness.context.measurementState.viewportMeasuredForKey = "scroll-view";
|
||||
harness.context.measurementState.contentMeasuredForKey = "scroll-view";
|
||||
harness.context.nearBottom = true;
|
||||
harness.driver.notifyAuthoritativeHistoryMaybeChanged();
|
||||
harness.driver.reevaluate();
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
blockedReason: null,
|
||||
pendingRequest: null,
|
||||
pendingVerification: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses sticky maintenance while detached", () => {
|
||||
const harness = createDriverHarness();
|
||||
|
||||
harness.driver.detachByUser();
|
||||
harness.driver.handleContentSizeChange({
|
||||
previousContentHeight: 1200,
|
||||
contentHeight: 1500,
|
||||
});
|
||||
harness.driver.handleViewportMetricsChange({
|
||||
previousViewportWidth: 800,
|
||||
viewportWidth: 640,
|
||||
previousViewportHeight: 480,
|
||||
viewportHeight: 420,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.driver.getSnapshot().mode).toBe("detached");
|
||||
expect(harness.scrollToBottom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("switches back to sticky-bottom for explicit jump-to-bottom", () => {
|
||||
const harness = createDriverHarness({
|
||||
isNearBottom: false,
|
||||
});
|
||||
|
||||
harness.driver.detachByUser();
|
||||
harness.driver.requestLocalAnchor({
|
||||
agentId: "agent-1",
|
||||
reason: "jump-to-bottom",
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.modeChanges).toContain("detached");
|
||||
expect(harness.modeChanges).toContain("sticky-bottom");
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
expect(harness.driver.getSnapshot().mode).toBe("sticky-bottom");
|
||||
});
|
||||
|
||||
it("schedules sticky maintenance on viewport and content growth", () => {
|
||||
const harness = createDriverHarness();
|
||||
|
||||
harness.driver.handleViewportMetricsChange({
|
||||
previousViewportWidth: 800,
|
||||
viewportWidth: 640,
|
||||
previousViewportHeight: 480,
|
||||
viewportHeight: 420,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
harness.driver.handleContentSizeChange({
|
||||
previousContentHeight: 1200,
|
||||
contentHeight: 1600,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps a pending request blocked when stale container measurements arrive", () => {
|
||||
const harness = createDriverHarness({
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "web-partial-virtualized",
|
||||
viewportHeight: 420,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
});
|
||||
|
||||
harness.driver.applyRouteRequest({
|
||||
agentId: "agent-1",
|
||||
reason: "resume",
|
||||
requestKey: "route:agent-1:resume",
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).not.toHaveBeenCalled();
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
blockedReason: "waiting_for_measurable_viewport",
|
||||
pendingRequest: {
|
||||
reason: "resume",
|
||||
},
|
||||
});
|
||||
|
||||
harness.context.measurementState.viewportMeasuredForKey = "web-partial-virtualized";
|
||||
harness.context.measurementState.contentMeasuredForKey = "web-partial-virtualized";
|
||||
harness.driver.reevaluate();
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
expect(harness.driver.getSnapshot().pendingRequest).toBeNull();
|
||||
});
|
||||
|
||||
it("uses delayed rechecks instead of repeated rescroll loops for native transport", () => {
|
||||
const harness = createDriverHarness({
|
||||
transportBehavior: {
|
||||
verificationDelayFrames: 2,
|
||||
verificationRetryMode: "recheck",
|
||||
},
|
||||
isNearBottom: false,
|
||||
});
|
||||
harness.scrollToBottom.mockImplementation(() => {
|
||||
harness.context.measurementState.offsetY = 0;
|
||||
});
|
||||
|
||||
harness.driver.requestLocalAnchor({
|
||||
agentId: "agent-1",
|
||||
reason: "jump-to-bottom",
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
harness.scheduler.flushFrame();
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
|
||||
harness.context.nearBottom = true;
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
expect(harness.driver.getSnapshot().pendingRequest).toBeNull();
|
||||
});
|
||||
|
||||
it("does not stay blocked on post-layout verification after a retry-scroll request", () => {
|
||||
const harness = createDriverHarness({
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "web-partial-virtualized",
|
||||
viewportWidth: 828,
|
||||
viewportHeight: 846,
|
||||
contentHeight: 14322,
|
||||
offsetY: 0,
|
||||
viewportMeasuredForKey: "web-partial-virtualized",
|
||||
contentMeasuredForKey: "web-partial-virtualized",
|
||||
}),
|
||||
isNearBottom: false,
|
||||
});
|
||||
|
||||
harness.scrollToBottom.mockImplementation(() => {
|
||||
harness.context.measurementState.offsetY = 13476;
|
||||
});
|
||||
|
||||
harness.driver.applyRouteRequest({
|
||||
agentId: "agent-1",
|
||||
reason: "resume",
|
||||
requestKey: "route:agent-1:resume",
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(1);
|
||||
|
||||
harness.context.measurementState.contentHeight = 14804;
|
||||
harness.context.nearBottom = false;
|
||||
harness.driver.handleContentSizeChange({
|
||||
previousContentHeight: 14322,
|
||||
contentHeight: 14804,
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
pendingRequest: {
|
||||
reason: "resume",
|
||||
},
|
||||
pendingVerification: {
|
||||
requestId: 1,
|
||||
retries: 1,
|
||||
},
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(2);
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
blockedReason: "waiting_for_post_layout_verification",
|
||||
pendingRequest: {
|
||||
reason: "resume",
|
||||
},
|
||||
pendingVerification: {
|
||||
requestId: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fulfill a web partial-virtualized resume request before a confirmation pass", () => {
|
||||
const harness = createDriverHarness({
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "web-partial-virtualized",
|
||||
viewportWidth: 828,
|
||||
viewportHeight: 846,
|
||||
contentHeight: 14322,
|
||||
offsetY: 0,
|
||||
viewportMeasuredForKey: "web-partial-virtualized",
|
||||
contentMeasuredForKey: "web-partial-virtualized",
|
||||
}),
|
||||
isNearBottom: false,
|
||||
});
|
||||
|
||||
harness.scrollToBottom.mockImplementation(() => {
|
||||
harness.context.measurementState.offsetY = Math.max(
|
||||
0,
|
||||
harness.context.measurementState.contentHeight -
|
||||
harness.context.measurementState.viewportHeight,
|
||||
);
|
||||
harness.context.nearBottom = true;
|
||||
});
|
||||
|
||||
harness.driver.applyRouteRequest({
|
||||
agentId: "agent-1",
|
||||
reason: "resume",
|
||||
requestKey: "route:agent-1:resume-confirmation",
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
harness.scheduler.flushFrame();
|
||||
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
pendingRequest: {
|
||||
reason: "resume",
|
||||
},
|
||||
blockedReason: "waiting_for_post_layout_verification",
|
||||
});
|
||||
|
||||
harness.context.measurementState.contentHeight = 16230;
|
||||
harness.context.nearBottom = false;
|
||||
harness.driver.handleContentSizeChange({
|
||||
previousContentHeight: 14322,
|
||||
contentHeight: 16230,
|
||||
});
|
||||
|
||||
harness.scheduler.flushFrame();
|
||||
harness.scheduler.flushFrame();
|
||||
harness.scheduler.flushFrame();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(2);
|
||||
expect(harness.driver.getSnapshot().pendingRequest).toMatchObject({
|
||||
reason: "resume",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps sticky-bottom during viewport growth until bottom is re-verified", () => {
|
||||
const harness = createDriverHarness();
|
||||
harness.context.nearBottom = false;
|
||||
harness.scrollToBottom.mockImplementation(() => {
|
||||
harness.context.measurementState.offsetY = 720;
|
||||
});
|
||||
|
||||
harness.driver.handleViewportMetricsChange({
|
||||
previousViewportWidth: 800,
|
||||
viewportWidth: 800,
|
||||
previousViewportHeight: 480,
|
||||
viewportHeight: 420,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(4);
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
mode: "sticky-bottom",
|
||||
pendingRequest: null,
|
||||
pendingVerification: null,
|
||||
});
|
||||
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
});
|
||||
|
||||
expect(harness.driver.getSnapshot().mode).toBe("sticky-bottom");
|
||||
|
||||
harness.context.nearBottom = true;
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: true,
|
||||
scrollDelta: 0,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 64,
|
||||
});
|
||||
|
||||
expect(harness.driver.getSnapshot().mode).toBe("detached");
|
||||
});
|
||||
|
||||
it("keeps sticky-bottom during streaming growth until bottom is re-verified", () => {
|
||||
const harness = createDriverHarness();
|
||||
harness.context.nearBottom = false;
|
||||
harness.scrollToBottom.mockImplementation(() => {
|
||||
harness.context.measurementState.offsetY = 900;
|
||||
});
|
||||
|
||||
harness.driver.handleContentSizeChange({
|
||||
previousContentHeight: 1200,
|
||||
contentHeight: 1400,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
|
||||
expect(harness.scrollToBottom).toHaveBeenCalledTimes(4);
|
||||
expect(harness.driver.getSnapshot()).toMatchObject({
|
||||
mode: "sticky-bottom",
|
||||
pendingRequest: null,
|
||||
pendingVerification: null,
|
||||
});
|
||||
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
});
|
||||
|
||||
expect(harness.driver.getSnapshot().mode).toBe("sticky-bottom");
|
||||
|
||||
harness.context.nearBottom = true;
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: true,
|
||||
scrollDelta: 0,
|
||||
});
|
||||
harness.scheduler.flushAll();
|
||||
harness.driver.handleScrollNearBottomChange({
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 64,
|
||||
});
|
||||
|
||||
expect(harness.driver.getSnapshot().mode).toBe("detached");
|
||||
});
|
||||
});
|
||||
|
||||
describe("controller helper predicates", () => {
|
||||
it("rejects stale container measurements during post-scroll verification", () => {
|
||||
expect(
|
||||
__private__.deriveVerificationBlockedReason({
|
||||
isAuthoritativeHistoryReady: true,
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "web-partial-virtualized",
|
||||
viewportHeight: 420,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
}),
|
||||
).toBe("waiting_for_measurable_viewport");
|
||||
});
|
||||
|
||||
it("allows verification only after authoritative readiness and current geometry exist", () => {
|
||||
expect(
|
||||
__private__.deriveVerificationBlockedReason({
|
||||
isAuthoritativeHistoryReady: false,
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "scroll-view",
|
||||
viewportHeight: 420,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
}),
|
||||
).toBe("waiting_for_history_readiness");
|
||||
|
||||
expect(
|
||||
__private__.deriveVerificationBlockedReason({
|
||||
isAuthoritativeHistoryReady: true,
|
||||
measurementState: createMeasurementState({
|
||||
containerKey: "scroll-view",
|
||||
viewportHeight: 420,
|
||||
contentHeight: 1200,
|
||||
viewportMeasuredForKey: "scroll-view",
|
||||
contentMeasuredForKey: "scroll-view",
|
||||
}),
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("suppresses auto-anchor helpers while detached", () => {
|
||||
const mode: BottomAnchorMode = "detached";
|
||||
|
||||
expect(
|
||||
__private__.shouldRestickOnContentChange({
|
||||
mode,
|
||||
previousContentHeight: 1000,
|
||||
contentHeight: 1100,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
__private__.shouldRestickOnViewportChange({
|
||||
mode,
|
||||
previousViewportWidth: 800,
|
||||
viewportWidth: 640,
|
||||
previousViewportHeight: 400,
|
||||
viewportHeight: 360,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not detach from sticky while a restick request is still pending", () => {
|
||||
expect(
|
||||
__private__.shouldDetachFromScrollAway({
|
||||
mode: "sticky-bottom",
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
hasPendingRequest: true,
|
||||
hasPendingVerification: false,
|
||||
hasUnverifiedStickyMeasurementChange: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
__private__.shouldDetachFromScrollAway({
|
||||
mode: "sticky-bottom",
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
hasPendingRequest: false,
|
||||
hasPendingVerification: true,
|
||||
hasUnverifiedStickyMeasurementChange: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
__private__.shouldDetachFromScrollAway({
|
||||
mode: "sticky-bottom",
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
hasPendingRequest: false,
|
||||
hasPendingVerification: false,
|
||||
hasUnverifiedStickyMeasurementChange: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
__private__.shouldDetachFromScrollAway({
|
||||
mode: "sticky-bottom",
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 0,
|
||||
hasPendingRequest: false,
|
||||
hasPendingVerification: false,
|
||||
hasUnverifiedStickyMeasurementChange: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("treats a large scroll delta as user detach even during an unverified sticky change", () => {
|
||||
expect(
|
||||
__private__.shouldDetachFromScrollAway({
|
||||
mode: "sticky-bottom",
|
||||
nextIsNearBottom: false,
|
||||
scrollDelta: 48,
|
||||
hasPendingRequest: false,
|
||||
hasPendingVerification: false,
|
||||
hasUnverifiedStickyMeasurementChange: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user