Shopify / flash-list

A better list for React Native
https://shopify.github.io/flash-list/
MIT License
5.64k stars 283 forks source link

Unit tests for horizontal list failing due to "hidden" list item #557

Open juusojpak opened 2 years ago

juusojpak commented 2 years ago

Current behavior

Using @testing-library/react-native, unit tests for a FlashList component with horizontal prop set as true, that expect specific amount of items to be found, are failing due to duplicate occurrence of the last list item in the rendered JSON:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`FlashList test renders correct amount of list items 1`] = `
<View
  style={Object {}}
>
  <RCTScrollView
    applyWindowCorrection={[Function]}
    canChangeSize={true}
    contentContainerStyle={
      Object {
        "backgroundColor": undefined,
        "minHeight": 1,
        "minWidth": 1,
      }
    }
    contentHeight={900}
    contentWidth={400}
    data={
      Array [
        Object {
          "title": "First Item",
        },
        Object {
          "title": "Second Item",
        },
      ]
    }
    dataProvider={
      DataProvider {
        "_data": Array [
          Object {
            "title": "First Item",
          },
          Object {
            "title": "Second Item",
          },
        ],
        "_firstIndexToProcess": 0,
        "_hasStableIds": true,
        "_requiresDataChangeHandling": false,
        "_size": 2,
        "getStableId": [Function],
        "rowHasChanged": [Function],
      }
    }
    disableRecycling={false}
    estimatedItemSize={200}
    extendedState={Object {}}
    externalScrollView={[Function]}
    finalRenderAheadOffset={250}
    forceNonDeterministicRendering={true}
    horizontal={true}
    initialOffset={0}
    initialRenderIndex={0}
    isHorizontal={true}
    layoutProvider={
      GridLayoutProviderWithProps {
        "_acceptableRelayoutDelta": 1,
        "_getHeightOrWidth": [Function],
        "_getLayoutTypeForIndex": [Function],
        "_getSpan": [Function],
        "_isHorizontal": true,
        "_lastLayoutManager": GridLayoutManager {
          "_acceptableRelayoutDelta": 1,
          "_getSpan": [Function],
          "_isGridHorizontal": true,
          "_isHorizontal": true,
          "_layoutProvider": [Circular],
          "_layouts": Array [
            Object {
              "height": 900,
              "type": 0,
              "width": 200,
              "x": 0,
              "y": 0,
            },
            Object {
              "height": 900,
              "type": 0,
              "width": 200,
              "x": 200,
              "y": 0,
            },
          ],
          "_maxSpan": 1,
          "_renderWindowSize": Object {
            "height": 900,
            "width": 400,
          },
          "_totalHeight": 900,
          "_totalWidth": 400,
          "_window": Object {
            "height": 900,
            "width": 400,
          },
        },
        "_maxSpan": 1,
        "_renderWindowSize": Object {
          "height": 900,
          "width": 400,
        },
        "_setLayoutForType": [Function],
        "_tempDim": Object {
          "height": 0,
          "width": 0,
        },
        "averageWindow": AverageWindow {
          "currentAverage": 200,
          "currentCount": 1,
          "inputValues": Array [
            200,
            undefined,
            undefined,
            undefined,
            undefined,
            undefined,
          ],
          "nextIndex": 1,
        },
        "defaultEstimatedItemSize": 100,
        "layoutObject": Object {
          "size": undefined,
          "span": undefined,
        },
        "props": Object {
          "data": Array [
            Object {
              "title": "First Item",
            },
            Object {
              "title": "Second Item",
            },
          ],
          "estimatedItemSize": 200,
          "horizontal": true,
          "numColumns": 1,
          "renderItem": [Function],
        },
        "shouldRefreshWithAnchoring": true,
      }
    }
    maxRenderAhead={750}
    numColumns={1}
    onEndReached={[Function]}
    onEndReachedThreshold={0}
    onEndReachedThresholdRelative={0}
    onItemLayout={[Function]}
    onLayout={[Function]}
    onScroll={[Function]}
    onScrollBeginDrag={[Function]}
    onSizeChanged={[Function]}
    onVisibleIndicesChanged={[Function]}
    removeClippedSubviews={false}
    renderAheadOffset={0}
    renderAheadStep={250}
    renderContentContainer={[Function]}
    renderItem={[Function]}
    renderItemContainer={[Function]}
    rowRenderer={[Function]}
    scrollEventThrottle={16}
    scrollThrottle={16}
    scrollViewProps={
      Object {
        "contentContainerStyle": Object {
          "backgroundColor": undefined,
          "minHeight": 1,
          "minWidth": 1,
        },
        "onLayout": [Function],
        "onScrollBeginDrag": [Function],
        "refreshControl": undefined,
        "style": Object {
          "minHeight": 1,
          "minWidth": 1,
        },
      }
    }
    style={
      Object {
        "minHeight": 1,
        "minWidth": 1,
      }
    }
    suppressBoundedSizeException={true}
    windowCorrectionConfig={
      Object {
        "applyToInitialOffset": true,
        "applyToItemScroll": true,
        "value": Object {
          "endCorrection": 0,
          "startCorrection": 0,
          "windowShift": -0,
        },
      }
    }
  >
    <View>
      <View
        style={
          Object {
            "flexDirection": "row",
          }
        }
      >
        <View
          style={
            Object {
              "paddingLeft": 0,
              "paddingTop": undefined,
            }
          }
        />
        <View
          style={
            Array [
              undefined,
              undefined,
            ]
          }
        />
        <AutoLayoutView
          enableInstrumentation={false}
          horizontal={true}
          onBlankAreaEvent={[Function]}
          onLayout={[Function]}
          renderAheadOffset={0}
          scrollOffset={0}
          style={
            Object {
              "height": 900,
              "width": 400,
            }
          }
          windowSize={400}
        >
          <CellContainer
            index={0}
            onLayout={[Function]}
            style={
              Object {
                "alignItems": "stretch",
                "flexDirection": "row",
                "height": 900,
                "left": 0,
                "position": "absolute",
                "top": 0,
              }
            }
          >
            <View
              style={
                Object {
                  "flexDirection": "column",
                }
              }
            >
              <Text>
                First Item
              </Text>
            </View>
          </CellContainer>
          <CellContainer
            index={1}
            onLayout={[Function]}
            style={
              Object {
                "alignItems": "stretch",
                "flexDirection": "row",
                "height": 900,
                "left": 200,
                "position": "absolute",
                "top": 0,
              }
            }
          >
            <View
              style={
                Object {
                  "flexDirection": "column",
                }
              }
            >
              <Text>
                Second Item
              </Text>
            </View>
          </CellContainer>
        </AutoLayoutView>
        <CellContainer
          index={-1}
          style={
            Array [
              undefined,
              undefined,
            ]
          }
        />
        <View
          style={
            Object {
              "paddingBottom": undefined,
              "paddingRight": 0,
            }
          }
        />
        <View
          pointerEvents="none"
          style={
            Object {
              "opacity": 0,
            }
          }
        >
          <Text>
            Second Item
          </Text>
        </View>
      </View>
    </View>
  </RCTScrollView>
</View>
`;

Expected behavior

Unit tests using getByText function from React Native Testing Library, as is shown in the testing example in the documentation, should pass for horizontal lists.

To Reproduce

Component file:

import React from 'react'
import { Text } from 'react-native'
import { FlashList } from '@shopify/flash-list'

export const FlashListTest = () => {
  const DATA = [
    { title: 'First Item' },
    { title: 'Second Item' },
  ]
  return (
    <FlashList data={DATA} renderItem={({ item }) => <Text>{item.title}</Text>} estimatedItemSize={200} horizontal />
  )
}

Test file:

import React from 'react'
import { render } from '@testing-library/react-native'
import { FlashListTest } from './FlashListTest'

describe('FlashList test', () => {
  beforeEach(jest.clearAllMocks)

  it('renders correct amount of list items', () => {
    const api = render(<FlashListTest />)
    api.getByText('First Item')
    api.getByText('Second Item') // <- THIS WILL FAIL
  })
})

Test will fail to:

Found multiple elements with text: Second Item

Platform:

Environment

1.2.1

naqvitalha commented 2 years ago

We actually do mount an extra item in horizontal mode to measure the list. Can you incorporate that in your test? We will update the documentation to include this point.

juusojpak commented 2 years ago

Great if you can add a mention about this to the documentation 👍

We worked around this by forcing mocked FlashLists to always have horizontal set as false in tests:

// jestSetup.js

jest.mock('@shopify/flash-list', () => {
  const React = require('react')
  const ActualFlashList = jest.requireActual('@shopify/flash-list').FlashList
  return {
    ...jest.requireActual('@shopify/flash-list'),
    FlashList: (props) => (
      <ActualFlashList {...props} estimatedListSize={{ height: 1000, width: 400 }} horizontal={false} />
    ),
  }
})
scott-harrison commented 4 months ago

Great if you can add a mention about this to the documentation 👍

We worked around this by forcing mocked FlashLists to always have horizontal set as false in tests:

// jestSetup.js

jest.mock('@shopify/flash-list', () => {
  const React = require('react')
  const ActualFlashList = jest.requireActual('@shopify/flash-list').FlashList
  return {
    ...jest.requireActual('@shopify/flash-list'),
    FlashList: (props) => (
      <ActualFlashList {...props} estimatedListSize={{ height: 1000, width: 400 }} horizontal={false} />
    ),
  }
})

I having same issue, when i try your workaround I seem to get a error TypeError Property declarations[0] of VariableDeclaration expected node to be of a type ["VariableDeclarator"] but instead got undefined

Anyone got any ideas?

scott-harrison commented 4 months ago

estimatedListSize={{ height: 1000, width: 400 }}

Found I had to do it like the following:

jest.mock('@shopify/flash-list', () => {
  const React = require('react')
  const ActualFlashList = jest.requireActual('@shopify/flash-list').FlashList

  class MockFlashList extends React.Component {
    componentDidMount() {
      if (super.componentDidMount) {
        super.componentDidMount()
      }
      this.rlvRef?._scrollComponent?._scrollViewRef?.props.onLayout({
        nativeEvent: { layout: { height: 900, width: 400 } },
      })
    }

    render() {
      const { horizontal, ...restProps } = this.props
      return <ActualFlashList {...restProps} estimatedListSize={{ height: 1000, width: 400 }} horizontal={false} />
    }
  }

  return {
    ...jest.requireActual('@shopify/flash-list'),
    FlashList: MockFlashList,
    AnimatedFlashList: MockFlashList,
  }
})