Skip to content
1 change: 1 addition & 0 deletions src/components/KeyboardChatScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ const KeyboardChatScrollView = forwardRef<
scroll,
layout,
size,
contentOffsetY,
inverted,
keyboardLiftBehavior,
freeze,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type Reanimated from "react-native-reanimated";
export const mockScrollTo = jest.fn();
export let reactionEffect: (current: number, previous: number | null) => void;

export const flushRAF = () => new Promise((resolve) => setTimeout(resolve, 0));

jest.mock("react-native-reanimated", () => ({
...require("react-native-reanimated/mock"),
scrollTo: (...args: unknown[]) => mockScrollTo(...args),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { sv } from "../../../../__fixtures__/sv";
import {
createRender,
flushRAF,
mockScrollTo,
reactionEffect,
} from "../__fixtures__/setup";

describe("useExtraContentPadding — always behavior", () => {
it("should scrollTo on grow when at end (non-inverted)", () => {
it("should scrollTo on grow when at end (non-inverted)", async () => {
const render = createRender();

render({
Expand All @@ -21,6 +22,7 @@ describe("useExtraContentPadding — always behavior", () => {
});

reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(
expect.anything(),
Expand All @@ -30,7 +32,7 @@ describe("useExtraContentPadding — always behavior", () => {
);
});

it("should scrollTo on grow when NOT at end (non-inverted)", () => {
it("should scrollTo on grow when NOT at end (non-inverted)", async () => {
const render = createRender();

render({
Expand All @@ -45,11 +47,12 @@ describe("useExtraContentPadding — always behavior", () => {
});

reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, 120, false);
});

it("should scrollTo on shrink (non-inverted)", () => {
it("should scrollTo on shrink (non-inverted)", async () => {
const render = createRender();

render({
Expand All @@ -64,6 +67,7 @@ describe("useExtraContentPadding — always behavior", () => {
});

reactionEffect(0, 20);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(
expect.anything(),
Expand All @@ -73,7 +77,7 @@ describe("useExtraContentPadding — always behavior", () => {
);
});

it("should scrollTo on grow (inverted)", () => {
it("should scrollTo on grow (inverted)", async () => {
const render = createRender();

render({
Expand All @@ -88,6 +92,7 @@ describe("useExtraContentPadding — always behavior", () => {
});

reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, -15, false);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sv } from "../../../../__fixtures__/sv";
import {
createRender,
flushRAF,
mockScrollTo,
reactionEffect,
} from "../__fixtures__/setup";
Expand Down Expand Up @@ -29,7 +30,7 @@ describe("useExtraContentPadding — blankSpace floor", () => {
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should scroll by effective delta when blankSpace partially absorbs", () => {
it("should scroll by effective delta when blankSpace partially absorbs", async () => {
const render = createRender();

render({
Expand All @@ -50,11 +51,12 @@ describe("useExtraContentPadding — blankSpace floor", () => {
// maxScroll = max(2000 - 800 + 500, 0) = 1700
// target = min(100 + 100, 1700) = 200
reactionEffect(300, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, 200, false);
});

it("blankSpace=0 produces identical behavior to default", () => {
it("blankSpace=0 produces identical behavior to default", async () => {
const render = createRender();

render({
Expand All @@ -75,6 +77,7 @@ describe("useExtraContentPadding — blankSpace floor", () => {
// maxScroll = max(2000 - 800 + 320, 0) = 1520
// target = min(1200 + 20, 1520) = 1220
reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(
expect.anything(),
Expand Down Expand Up @@ -107,7 +110,7 @@ describe("useExtraContentPadding — blankSpace floor", () => {
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should scroll when change exceeds blankSpace floor (inverted)", () => {
it("should scroll when change exceeds blankSpace floor (inverted)", async () => {
const render = createRender();

render({
Expand All @@ -127,6 +130,7 @@ describe("useExtraContentPadding — blankSpace floor", () => {
// effectiveDelta = 100
// target = max(5 - 100, -500) = max(-95, -500) = -95
reactionEffect(200, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, -95, false);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { sv } from "../../../../__fixtures__/sv";
import {
createRender,
flushRAF,
mockScrollTo,
reactionEffect,
} from "../__fixtures__/setup";

describe("useExtraContentPadding — contentOffsetY (iOS atomic path)", () => {
it("should set contentOffsetY instead of scrollTo on grow (non-inverted)", () => {
const render = createRender();
const contentOffsetY = sv(100);

render({
extraContentPadding: sv(20),
keyboardPadding: sv(300),
scroll: sv(100),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY,
inverted: false,
keyboardLiftBehavior: "always",
freeze: false,
});

reactionEffect(20, 0);

expect(contentOffsetY.value).toBe(120);
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should set contentOffsetY instead of scrollTo on grow (inverted)", () => {
const render = createRender();
const contentOffsetY = sv(5);

render({
extraContentPadding: sv(20),
keyboardPadding: sv(300),
scroll: sv(5),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY,
inverted: true,
keyboardLiftBehavior: "always",
freeze: false,
});

reactionEffect(20, 0);

expect(contentOffsetY.value).toBe(-15);
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should set contentOffsetY instead of scrollTo on shrink (non-inverted)", () => {
const render = createRender();
const contentOffsetY = sv(1220);

render({
extraContentPadding: sv(0),
keyboardPadding: sv(300),
scroll: sv(1220),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY,
inverted: false,
keyboardLiftBehavior: "always",
freeze: false,
});

reactionEffect(0, 20);

expect(contentOffsetY.value).toBe(1200);
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should clamp contentOffsetY to maxScroll (non-inverted)", () => {
const render = createRender();
const contentOffsetY = sv(1490);

render({
extraContentPadding: sv(50),
keyboardPadding: sv(300),
scroll: sv(1490),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY,
inverted: false,
keyboardLiftBehavior: "always",
freeze: false,
});

// delta = 50, scroll + delta = 1540, maxScroll = 2000 - 800 + 300 + 50 = 1550
reactionEffect(50, 0);

expect(contentOffsetY.value).toBe(1540);
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should clamp contentOffsetY to -totalPadding (inverted)", () => {
const render = createRender();
const contentOffsetY = sv(-280);

render({
extraContentPadding: sv(50),
keyboardPadding: sv(300),
scroll: sv(-280),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY,
inverted: true,
keyboardLiftBehavior: "always",
freeze: false,
});

// delta = 50, target = -280 - 50 = -330, clamp to -350
reactionEffect(50, 0);

expect(contentOffsetY.value).toBe(-330);
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should fall back to scrollTo when contentOffsetY is undefined", async () => {
const render = createRender();

render({
extraContentPadding: sv(20),
keyboardPadding: sv(300),
scroll: sv(100),
layout: sv({ width: 390, height: 800 }),
size: sv({ width: 390, height: 2000 }),
contentOffsetY: undefined,
inverted: false,
keyboardLiftBehavior: "always",
freeze: false,
});

reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, 120, false);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { sv } from "../../../../__fixtures__/sv";
import {
createRender,
flushRAF,
mockScrollTo,
reactionEffect,
} from "../__fixtures__/setup";
Expand Down Expand Up @@ -63,7 +64,7 @@ describe("useExtraContentPadding — edge cases", () => {
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should clamp to maxScroll (non-inverted)", () => {
it("should clamp to maxScroll (non-inverted)", async () => {
const render = createRender();

render({
Expand All @@ -79,6 +80,7 @@ describe("useExtraContentPadding — edge cases", () => {

// delta = 50, scroll + delta = 1540, maxScroll = 2000 - 800 + 300 + 50 = 1550
reactionEffect(50, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(
expect.anything(),
Expand All @@ -88,7 +90,7 @@ describe("useExtraContentPadding — edge cases", () => {
);
});

it("should clamp to -totalPadding (inverted)", () => {
it("should clamp to -totalPadding (inverted)", async () => {
const render = createRender();

render({
Expand All @@ -104,6 +106,7 @@ describe("useExtraContentPadding — edge cases", () => {

// delta = 50, target = -280 - 50 = -330, clamp to -350
reactionEffect(50, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(
expect.anything(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { sv } from "../../../../__fixtures__/sv";
import {
createRender,
flushRAF,
mockScrollTo,
reactionEffect,
} from "../__fixtures__/setup";

describe("useExtraContentPadding — persistent behavior", () => {
it("should scrollTo on grow", () => {
it("should scrollTo on grow", async () => {
const render = createRender();

render({
Expand All @@ -21,6 +22,7 @@ describe("useExtraContentPadding — persistent behavior", () => {
});

reactionEffect(20, 0);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalledWith(expect.anything(), 0, 120, false);
});
Expand All @@ -44,7 +46,7 @@ describe("useExtraContentPadding — persistent behavior", () => {
expect(mockScrollTo).not.toHaveBeenCalled();
});

it("should scrollTo on shrink when at end", () => {
it("should scrollTo on shrink when at end", async () => {
const render = createRender();

render({
Expand All @@ -59,6 +61,7 @@ describe("useExtraContentPadding — persistent behavior", () => {
});

reactionEffect(0, 20);
await flushRAF();

expect(mockScrollTo).toHaveBeenCalled();
});
Expand Down
Loading
Loading