stream-chat-react-native-core
Version:
The official React Native and Expo components for Stream Chat, a service for building chat applications
218 lines (189 loc) • 7.43 kB
text/typescript
import { Dimensions, Platform, View } from 'react-native';
import type { EdgeInsets } from 'react-native-safe-area-context';
import { isMeasuredRectBogus, measureInWindow } from '../measureInWindow';
// `measureInWindow` is globally mocked in jest-setup so other suites don't hit native
// measurement; this suite exercises the real implementation.
jest.unmock('../measureInWindow');
// measureInWindow reads `isRN86OrGreater` as a live binding at call time, so mocking the constants
// module with a mutable flag lets us exercise both the "RN handles the inset" and "we compensate"
// branches without reimporting. (Flag is `mock`-prefixed as jest requires for values referenced
// inside a mock factory.)
let mockIsRN86OrGreater = false;
jest.mock('../../../../utils/react-native-constants', () => ({
__esModule: true,
get isRN86OrGreater() {
return mockIsRN86OrGreater;
},
}));
// screen 400x800 => bogus threshold is |x| > 800 and |y| > 1600 (2x each)
const SCREEN = { fontScale: 1, height: 800, scale: 2, width: 400 };
const INSETS: EdgeInsets = { bottom: 10, left: 0, right: 0, top: 24 };
const setPlatform = (os: typeof Platform.OS) => {
Object.defineProperty(Platform, 'OS', { configurable: true, get: () => os });
};
type MeasureInWindowTuple = [number, number, number, number];
type MeasureTuple = [number, number, number, number, number, number];
/**
* Builds a fake native handle whose `measureInWindow`/`measure` callbacks fire synchronously
* (as Fabric does). Omitting `measure` produces a handle without the method, exercising the
* "no fallback available" branch.
*/
const makeNode = ({
measure,
measureInWindow: miw,
}: {
measure?: MeasureTuple;
measureInWindow: MeasureInWindowTuple;
}): { current: View | null } => {
const handle: Record<string, unknown> = {
measureInWindow: (cb: (...args: MeasureInWindowTuple) => void) => cb(...miw),
};
if (measure) {
handle.measure = (cb: (...args: MeasureTuple) => void) => cb(...measure);
}
return { current: handle as unknown as View };
};
describe('isMeasuredRectBogus', () => {
beforeEach(() => {
jest.spyOn(Dimensions, 'get').mockReturnValue(SCREEN);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('accepts a normal on-screen rect', () => {
expect(isMeasuredRectBogus(50, 300, 200, 40)).toBe(false);
});
it('accepts a rect within the 2x margin (e.g. partially off-screen)', () => {
expect(isMeasuredRectBogus(50, 1500, 200, 40)).toBe(false);
});
it.each([
['NaN x', [NaN, 10, 20, 20]],
['NaN y', [10, NaN, 20, 20]],
['Infinity x', [Infinity, 10, 20, 20]],
['-Infinity y', [10, -Infinity, 20, 20]],
['NaN width', [10, 10, NaN, 20]],
['Infinity height', [10, 10, 20, Infinity]],
] as [string, MeasureInWindowTuple][])('flags non-finite values: %s', (_label, [x, y, w, h]) => {
expect(isMeasuredRectBogus(x, y, w, h)).toBe(true);
});
it.each([
['zero width', [10, 10, 0, 40]],
['zero height', [10, 10, 40, 0]],
['negative width', [10, 10, -5, 40]],
['negative height', [10, 10, 40, -5]],
] as [string, MeasureInWindowTuple][])(
'flags non-positive dimensions: %s',
(_label, [x, y, w, h]) => {
expect(isMeasuredRectBogus(x, y, w, h)).toBe(true);
},
);
it.each([
['huge y (the FLAG_LAYOUT_NO_LIMITS failure)', [50, 29000, 40, 36]],
['huge x', [29000, 300, 40, 36]],
['huge negative x', [-2000, 300, 40, 36]],
['huge negative y', [50, -2000, 40, 36]],
] as [string, MeasureInWindowTuple][])(
'flags coordinates beyond 2x the screen: %s',
(_label, [x, y, w, h]) => {
expect(isMeasuredRectBogus(x, y, w, h)).toBe(true);
},
);
it('treats the exact 2x boundary as valid and just past it as bogus', () => {
// bounds are 800 (x) and 1600 (y); the check uses strict `>`
expect(isMeasuredRectBogus(800, 1600, 40, 36)).toBe(false);
expect(isMeasuredRectBogus(801, 300, 40, 36)).toBe(true);
expect(isMeasuredRectBogus(50, 1601, 40, 36)).toBe(true);
});
it('trusts the measurement when screen dimensions are unavailable', () => {
jest.spyOn(Dimensions, 'get').mockReturnValue({ ...SCREEN, height: 0, width: 0 });
expect(isMeasuredRectBogus(99999, 99999, 40, 36)).toBe(false);
});
});
describe('measureInWindow', () => {
const originalOS = Platform.OS;
beforeEach(() => {
jest.spyOn(Dimensions, 'get').mockReturnValue(SCREEN);
});
afterEach(() => {
jest.restoreAllMocks();
setPlatform(originalOS);
mockIsRN86OrGreater = false;
});
it('rejects when the node is not mounted', async () => {
await expect(measureInWindow({ current: null }, INSETS)).rejects.toThrow(/native handle/);
});
describe('healthy measurement (primary path)', () => {
it('resolves with the window rect unchanged on iOS', async () => {
setPlatform('ios');
const node = makeNode({ measureInWindow: [10, 300, 200, 40] });
await expect(measureInWindow(node, INSETS)).resolves.toEqual({
h: 40,
w: 200,
x: 10,
y: 300,
});
});
it('compensates by insets.top on Android below RN 0.86', async () => {
setPlatform('android');
mockIsRN86OrGreater = false;
const node = makeNode({ measureInWindow: [10, 300, 200, 40] });
await expect(measureInWindow(node, INSETS)).resolves.toEqual({
h: 40,
w: 200,
x: 10,
y: 324,
});
});
it('does not compensate on Android from RN 0.86 onward (RN corrects the inset natively)', async () => {
setPlatform('android');
mockIsRN86OrGreater = true;
const node = makeNode({ measureInWindow: [10, 300, 200, 40] });
await expect(measureInWindow(node, INSETS)).resolves.toEqual({
h: 40,
w: 200,
x: 10,
y: 300,
});
});
});
describe('bogus measurement (measure() fallback)', () => {
it('falls back to root-relative pageX/pageY', async () => {
setPlatform('ios');
const node = makeNode({
measure: [0, 0, 64, 36, 12, 717],
measureInWindow: [28903, 29088, 64, 36],
});
await expect(measureInWindow(node, INSETS)).resolves.toEqual({ h: 36, w: 64, x: 12, y: 717 });
});
it('does not add insets.top to the fallback, since pageY already accounts for it', async () => {
setPlatform('android');
const node = makeNode({
measure: [0, 0, 64, 36, 12, 717],
measureInWindow: [28903, 29088, 64, 36],
});
await expect(measureInWindow(node, INSETS)).resolves.toEqual({ h: 36, w: 64, x: 12, y: 717 });
});
it('returns the compensated window rect when no measure() fallback is available (RN < 0.86)', async () => {
setPlatform('android');
mockIsRN86OrGreater = false;
const node = makeNode({ measureInWindow: [28903, 29088, 64, 36] });
await expect(measureInWindow(node, INSETS)).resolves.toEqual({
h: 36,
w: 64,
x: 28903,
y: 29112,
});
});
it('returns the uncompensated window rect when no measure() fallback is available (RN >= 0.86)', async () => {
setPlatform('android');
mockIsRN86OrGreater = true;
const node = makeNode({ measureInWindow: [28903, 29088, 64, 36] });
await expect(measureInWindow(node, INSETS)).resolves.toEqual({
h: 36,
w: 64,
x: 28903,
y: 29088,
});
});
});
});