From 4bf24bba5b670f83beb9b847ac14440cce9ece45 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Wed, 29 Jul 2026 10:58:15 -0400 Subject: [PATCH] fix(rokt): prevent layout views from collapsing to zero height Replace flex sizing with cross-axis stretching so SDK-provided heights remain effective inside auto-height column containers. Add iOS and Android style coverage and configure Jest to compile TSX for the regression test. #agentic --- jest.config.js | 12 +++ js/__tests__/rokt-layout-view-style.test.tsx | 88 ++++++++++++++++++++ js/rokt/rokt-layout-view.android.tsx | 8 +- js/rokt/rokt-layout-view.ios.tsx | 8 +- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 js/__tests__/rokt-layout-view-style.test.tsx diff --git a/jest.config.js b/jest.config.js index 666f90a7..de808729 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,4 +2,16 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', testMatch: ['/js/**/*.test.ts?(x)'], + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + // tsconfig sets `jsx: react-native`, which leaves JSX untransformed for + // Metro to handle. Jest has no Metro, so compile it here. `react` is a peer + // dependency and is not installed, so use the classic runtime and let tests + // mock `react` rather than requiring `react/jsx-runtime`. + tsconfig: { jsx: 'react' }, + }, + ], + }, }; diff --git a/js/__tests__/rokt-layout-view-style.test.tsx b/js/__tests__/rokt-layout-view-style.test.tsx new file mode 100644 index 00000000..54d628ae --- /dev/null +++ b/js/__tests__/rokt-layout-view-style.test.tsx @@ -0,0 +1,88 @@ +/** + * `RoktLayoutView` sizes its native view from the height the Rokt SDK measures and + * pushes back to JS. If the style also sets `flex: 1`, that expands to + * `flexBasis: 0%`, which takes precedence over `height` on the parent's main axis — + * so inside any auto-height column parent the layout collapses to 0 and the + * placement is never visible, even though it selected successfully and reported a + * height. Guard both platform implementations against that regression. + */ +const mockAddListener = jest.fn(() => ({ remove: jest.fn() })); + +// `react` and `react-native` are peer dependencies and are not installed here, so +// stub the pieces these components touch. `createElement` just records what the +// component asked for, which is all the assertions below need. +jest.mock( + 'react', + () => { + class Component { + props: P; + state!: S; + constructor(props: P) { + this.props = props; + } + setState(partial: Partial) { + this.state = { ...this.state, ...partial }; + } + } + const createElement = ( + type: unknown, + props: Record | null + ) => ({ type, props: props ?? {} }); + return { __esModule: true, default: { createElement }, Component }; + }, + { virtual: true } +); + +jest.mock( + 'react-native', + () => ({ + // Pass styles through unchanged so the test can read the resolved values. + StyleSheet: { create: (styles: unknown) => styles }, + NativeModules: { RoktEventManager: {} }, + NativeEventEmitter: jest.fn(() => ({ addListener: mockAddListener })), + requireNativeComponent: jest.fn(() => 'RoktLegacyLayout'), + TurboModuleRegistry: { getEnforcing: jest.fn(() => ({})) }, + }), + { virtual: true } +); + +jest.mock( + '../codegenSpecs/rokt/RoktLayoutNativeComponent', + () => ({ __esModule: true, default: 'RoktNativeLayout' }), + { virtual: true } +); + +import { RoktLayoutView as IosLayoutView } from '../rokt/rokt-layout-view.ios'; +import { RoktLayoutView as AndroidLayoutView } from '../rokt/rokt-layout-view.android'; + +type LayoutViewCtor = typeof IosLayoutView | typeof AndroidLayoutView; + +/** Renders the component and flattens the style array it hands the native view. */ +function resolvedStyle(Ctor: LayoutViewCtor, height: number) { + const instance = new Ctor({ placeholderName: 'Location1' }); + // The measured height normally arrives via a native event; set it directly so the + // test does not depend on the platform-specific event plumbing. + instance.state = { ...instance.state, height }; + const element = instance.render() as React.ReactElement<{ style: object[] }>; + return Object.assign({}, ...element.props.style) as Record; +} + +describe.each([ + ['ios', IosLayoutView], + ['android', AndroidLayoutView], +])('RoktLayoutView (%s) style', (_platform, Ctor) => { + it('applies the measured height', () => { + expect(resolvedStyle(Ctor, 530)).toMatchObject({ height: 530 }); + }); + + it('does not set flex, which would override the height and collapse the view', () => { + const style = resolvedStyle(Ctor, 530); + expect(style.flex).toBeUndefined(); + expect(style.flexBasis).toBeUndefined(); + expect(style.flexGrow).toBeUndefined(); + }); + + it('stretches to the full available width', () => { + expect(resolvedStyle(Ctor, 530)).toMatchObject({ alignSelf: 'stretch' }); + }); +}); diff --git a/js/rokt/rokt-layout-view.android.tsx b/js/rokt/rokt-layout-view.android.tsx index b06cad39..fa31a696 100644 --- a/js/rokt/rokt-layout-view.android.tsx +++ b/js/rokt/rokt-layout-view.android.tsx @@ -29,7 +29,13 @@ interface HeightChangedEvent { const styles = StyleSheet.create({ widget: { - flex: 1, + // Do NOT use `flex: 1` here. It expands to `flexBasis: 0%`, which takes + // precedence over `height` on the parent's main axis, so inside any + // auto-height column parent the layout collapses to 0 and the placement is + // never visible even though it was selected and reported its height. + // `alignSelf: 'stretch'` gives the full available width without touching the + // main axis, leaving the measured `height` free to apply. + alignSelf: 'stretch', backgroundColor: 'transparent', }, }); diff --git a/js/rokt/rokt-layout-view.ios.tsx b/js/rokt/rokt-layout-view.ios.tsx index 05754d92..a8546a8b 100644 --- a/js/rokt/rokt-layout-view.ios.tsx +++ b/js/rokt/rokt-layout-view.ios.tsx @@ -88,7 +88,13 @@ export class RoktLayoutView extends Component< const styles = StyleSheet.create({ widget: { - flex: 1, + // Do NOT use `flex: 1` here. It expands to `flexBasis: 0%`, which takes + // precedence over `height` on the parent's main axis, so inside any + // auto-height column parent the layout collapses to 0 and the placement is + // never visible even though it was selected and reported its height. + // `alignSelf: 'stretch'` gives the full available width without touching the + // main axis, leaving the measured `height` free to apply. + alignSelf: 'stretch', backgroundColor: 'transparent', overflow: 'hidden', },