Add detailed logging to bottom anchor controller and scroll strategy

This commit is contained in:
Mohamed Boudra
2026-03-08 20:48:37 +07:00
parent 87948e956a
commit 7c6eb2ad74
3 changed files with 132 additions and 8 deletions
@@ -0,0 +1,30 @@
import { defineConfig, devices } from "@playwright/test";
const baseURL =
process.env.E2E_BASE_URL ??
`http://localhost:${process.env.E2E_METRO_PORT ?? "8081"}`;
export default defineConfig({
testDir: "./e2e",
globalSetup: "./e2e/global-setup.ts",
timeout: 60_000,
expect: {
timeout: 10_000,
},
fullyParallel: false,
workers: 1,
retries: 0,
reporter: [["list"]],
use: {
baseURL,
trace: "retain-on-failure",
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "Desktop Safari",
use: { ...devices["Desktop Safari"] },
},
],
});
@@ -245,6 +245,32 @@ function forceScrollContainerToBottom(
}
}
function logWebBottomAnchorTransport(
event: string,
details: Record<string, unknown>
): void {
if (!(globalThis as { __DEV__?: boolean }).__DEV__) {
return;
}
console.debug("[BottomAnchorTransport]", event, details);
}
function getResolvedWebScrollContainerMetrics(
refs: StreamRenderRefs
): Record<string, unknown> {
const scrollNode = resolveWebScrollContainerNode(refs);
if (!scrollNode) {
return { hasScrollNode: false };
}
return {
hasScrollNode: true,
scrollTop: scrollNode.scrollTop,
scrollHeight: scrollNode.scrollHeight,
clientHeight: scrollNode.clientHeight,
tagName: scrollNode.tagName,
};
}
function createStreamRenderStrategy(
config: StreamRenderStrategyConfig
): StreamRenderStrategy {
@@ -376,12 +402,28 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
0,
metrics.contentHeight - metrics.viewportHeight
);
logWebBottomAnchorTransport("forward_scroll_to_bottom_start", {
animated,
bottomOffset,
metrics,
usedFlatList: Boolean(refs.flatListRef.current),
...getResolvedWebScrollContainerMetrics(refs),
});
if (refs.flatListRef.current) {
refs.flatListRef.current.scrollToEnd?.({ animated });
refs.flatListRef.current.scrollToOffset?.({
offset: bottomOffset,
animated,
});
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
logWebBottomAnchorTransport("forward_scroll_to_bottom_after_flatlist", {
animated,
bottomOffset,
...getResolvedWebScrollContainerMetrics(refs),
});
});
}
return;
}
const usedAnchor = scrollAnchorIntoView({ refs, animated });
@@ -394,6 +436,16 @@ function createForwardStreamStrategy(): StreamRenderStrategy {
animated,
});
forceScrollContainerToBottom(refs, bottomOffset);
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
logWebBottomAnchorTransport("forward_scroll_to_bottom_after_scrollview", {
animated,
bottomOffset,
usedAnchor,
...getResolvedWebScrollContainerMetrics(refs),
});
});
}
},
scrollToOffset: ({ refs, offset, animated }) => {
if (refs.flatListRef.current) {
@@ -233,6 +233,20 @@ function deriveRetryDisposition(input: {
: "retry-scroll";
}
function getDetailedMeasurementState(
measurementState: ControllerMeasurementState
): Record<string, unknown> {
return {
containerKey: measurementState.containerKey,
viewportWidth: measurementState.viewportWidth,
viewportHeight: measurementState.viewportHeight,
contentHeight: measurementState.contentHeight,
offsetY: measurementState.offsetY,
viewportMeasuredForKey: measurementState.viewportMeasuredForKey,
contentMeasuredForKey: measurementState.contentMeasuredForKey,
};
}
function createBottomAnchorControllerDriver(
input: CreateBottomAnchorControllerDriverInput
): BottomAnchorControllerDriver {
@@ -344,18 +358,37 @@ function createBottomAnchorControllerDriver(
});
if (verificationBlockedReason) {
input.log(
"attempt_verified",
getLogContext({
verificationPhase: "blocked",
verificationBlockedReason,
retries: attemptContext.retries,
measurementState: getDetailedMeasurementState(measurementState),
})
);
pendingVerification = attemptContext;
setBlockedReason(verificationBlockedReason);
return;
}
const verifiedNearBottom = input.isNearBottom();
const retryDisposition = verifiedNearBottom
? null
: deriveRetryDisposition({
mode,
retries: attemptContext.retries,
verificationRetryMode:
input.getTransportBehavior().verificationRetryMode,
});
input.log(
"attempt_verified",
getLogContext({
verifiedNearBottom,
retries: attemptContext.retries,
retryDisposition,
measurementState: getDetailedMeasurementState(measurementState),
})
);
@@ -370,12 +403,6 @@ function createBottomAnchorControllerDriver(
return;
}
const retryDisposition = deriveRetryDisposition({
mode,
retries: attemptContext.retries,
verificationRetryMode: input.getTransportBehavior().verificationRetryMode,
});
if (retryDisposition === "retry-verify") {
pendingVerification = {
requestId: attemptContext.requestId,
@@ -397,7 +424,11 @@ function createBottomAnchorControllerDriver(
input.log(
"attempt_failed",
getLogContext({ retries: attemptContext.retries })
getLogContext({
retries: attemptContext.retries,
retryDisposition,
measurementState: getDetailedMeasurementState(measurementState),
})
);
pendingVerification = null;
if (isRequestAttempt && currentRequest) {
@@ -443,10 +474,11 @@ function createBottomAnchorControllerDriver(
kind: "attempt",
callback: () => {
attemptHandle = null;
const measurementState = input.getMeasurementState();
const nextBlockedReason = deriveBottomAnchorBlockedReason({
pendingRequest,
isAuthoritativeHistoryReady: input.getIsAuthoritativeHistoryReady(),
measurementState: input.getMeasurementState(),
measurementState,
pendingVerificationRequestId: pendingVerification?.requestId ?? null,
});
setBlockedReason(nextBlockedReason);
@@ -462,6 +494,16 @@ function createBottomAnchorControllerDriver(
!shouldAttemptForPendingRequest &&
!shouldAttemptForStickyVerification
) {
input.log(
"attempt_started",
getLogContext({
attemptPhase: "skipped",
nextBlockedReason,
shouldAttemptForPendingRequest,
shouldAttemptForStickyVerification,
measurementState: getDetailedMeasurementState(measurementState),
})
);
return;
}