From 9eec875a17f520df9e2f7270ba9d03d527db47ee Mon Sep 17 00:00:00 2001 From: Nick Gerleman Date: Fri, 4 Aug 2023 10:37:57 -0700 Subject: [PATCH] ListMetricAggregator UTs - cell measurement (#38735) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/38735 UTs around cell measurement results through `getCellMetrics` and `getCellMetricsApprox`. For each orientation, validate basic scenarios for approximation, cached measurement, or measurement by user-provided `getItemLayout`. Changelog: [Internal] Reviewed By: rozele Differential Revision: D47978630 fbshipit-source-id: 4c9ba6a60599848034f4f23cde3497dd4c2b8788 --- .../__tests__/ListMetricsAggregator-test.js | 586 ++++++++++++++++++ 1 file changed, 586 insertions(+) diff --git a/packages/virtualized-lists/Lists/__tests__/ListMetricsAggregator-test.js b/packages/virtualized-lists/Lists/__tests__/ListMetricsAggregator-test.js index 6bbf4c3b99c..d36930d9b01 100644 --- a/packages/virtualized-lists/Lists/__tests__/ListMetricsAggregator-test.js +++ b/packages/virtualized-lists/Lists/__tests__/ListMetricsAggregator-test.js @@ -126,4 +126,590 @@ describe('ListMetricsAggregator', () => { }); expect(listMetrics.getHighestMeasuredCellIndex()).toEqual(1); }); + + it('resets measurements if list orientation changes', () => { + const listMetrics = new ListMetricsAggregator(); + expect(listMetrics.getAverageCellLength()).toEqual(0); + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation: {horizontal: false, rtl: false}, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + expect(listMetrics.getAverageCellLength()).toEqual(10); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation: {horizontal: true, rtl: false}, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + expect(listMetrics.getAverageCellLength()).toEqual(5); + }); + + it('resolves metrics of already measured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + expect(listMetrics.getCellMetricsApprox(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + }); + + it('estimates metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toBeNull(); + expect(listMetrics.getCellMetricsApprox(2, props)).toEqual({ + index: 2, + length: 15, + offset: 30, + isMounted: false, + }); + }); + + it('uses getItemLayout for metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + getItemLayout: () => ({index: 2, length: 40, offset: 30}), + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + expect(listMetrics.getCellMetricsApprox(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + }); + + it('resolves horizontal metrics of already measured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 10, + y: 0, + }, + }); + + expect(listMetrics.getCellMetrics(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + expect(listMetrics.getCellMetricsApprox(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + }); + + it('estimates horizontal metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 10, + y: 0, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toBeNull(); + expect(listMetrics.getCellMetricsApprox(2, props)).toEqual({ + index: 2, + length: 15, + offset: 30, + isMounted: false, + }); + }); + + it('uses getItemLayout for horizontal metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + getItemLayout: () => ({index: 2, length: 40, offset: 30}), + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 10, + y: 0, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + expect(listMetrics.getCellMetricsApprox(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + }); + + it('resolves RTL metrics of already measured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: true}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 90, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 70, + y: 0, + }, + }); + + listMetrics.notifyListContentLayout({ + layout: {width: 100, height: 5}, + orientation, + }); + + expect(listMetrics.getCellMetrics(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + expect(listMetrics.getCellMetricsApprox(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + }); + + it('estimates RTL metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 90, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 70, + y: 0, + }, + }); + + listMetrics.notifyListContentLayout({ + layout: {width: 100, height: 5}, + orientation, + }); + + expect(listMetrics.getCellMetrics(2, props)).toBeNull(); + expect(listMetrics.getCellMetricsApprox(2, props)).toEqual({ + index: 2, + length: 15, + offset: 30, + isMounted: false, + }); + }); + + it('uses getItemLayout for RTL metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: true, rtl: false}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + getItemLayout: () => ({index: 2, length: 40, offset: 30}), + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 5, + width: 10, + x: 90, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 5, + width: 20, + x: 70, + y: 0, + }, + }); + + listMetrics.notifyListContentLayout({ + layout: {width: 100, height: 5}, + orientation, + }); + + expect(listMetrics.getCellMetrics(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + expect(listMetrics.getCellMetricsApprox(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + }); + + it('resolves vertical rtl metrics of already measured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: true}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + expect(listMetrics.getCellMetricsApprox(1, props)).toEqual({ + index: 1, + length: 20, + offset: 10, + isMounted: true, + }); + }); + + it('estimates vertical RTL metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: true}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toBeNull(); + expect(listMetrics.getCellMetricsApprox(2, props)).toEqual({ + index: 2, + length: 15, + offset: 30, + isMounted: false, + }); + }); + + it('uses getItemLayout for vertical RTL metrics of unmeasured cell', () => { + const listMetrics = new ListMetricsAggregator(); + const orientation = {horizontal: false, rtl: true}; + const props: CellMetricProps = { + data: [1, 2, 3, 4, 5], + getItemCount: () => nullthrows(props.data).length, + getItem: (i: number) => nullthrows(props.data)[i], + getItemLayout: () => ({index: 2, length: 40, offset: 30}), + }; + + listMetrics.notifyCellLayout({ + cellIndex: 0, + cellKey: '0', + orientation, + layout: { + height: 10, + width: 5, + x: 0, + y: 0, + }, + }); + + listMetrics.notifyCellLayout({ + cellIndex: 1, + cellKey: '1', + orientation, + layout: { + height: 20, + width: 5, + x: 0, + y: 10, + }, + }); + + expect(listMetrics.getCellMetrics(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + expect(listMetrics.getCellMetricsApprox(2, props)).toMatchObject({ + index: 2, + length: 40, + offset: 30, + }); + }); });