UNPKG

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
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, }); }); }); });