-2

I have been debugging this and started losing hair. So far I haven't found a solution yet. This the Teaser component. I was initally writing tests for Home component, but it had some errors because of styled-components, so my techlead told me to write tests for this new Teaser component (which is the component that was new in Home component), because maybe it is having some effect. When running the Teaser.test.tsx I get this error (related to keys):

 FAIL  src/features/home/Teaser.test.tsx (6.781s)
  Teaser component
    × renders Teaser component when user has tonieboxes (185ms)

  ● Teaser component › renders Teaser component when user has tonieboxes

    expect(jest.fn()).not.toBeCalled()

    Expected number of calls: 0
    Received number of calls: 1

    1: "Warning: Each child in a list should have a unique \"key\" prop, "·
    Check the render method of `Teaser`.", "", "
        in Fragment (created by Teaser)
        in Teaser (at Teaser.test.tsx:16)
        in I18nextProvider (at test-utils/index.jsx:38)
        in AuthProvider (at test-utils/index.jsx:37)
        in ConfigProvider (at test-utils/index.jsx:36)
        in ThemeProvider (at test-utils/index.jsx:34)
        in Router (created by MemoryRouter)
        in MemoryRouter (at test-utils/index.jsx:33)
        in Providers"

      41 | // eslint-disable-next-line jest/no-duplicate-hooks
      42 | afterEach(() => {
    > 43 |   expect(console.error).not.toBeCalled()
         |                             ^
      44 |   expect(console.warn).not.toBeCalled()
      45 | 
      46 |   // Reset any request handlers that we may add during the tests,

      at Object.<anonymous> (src/setupTests.js:43:29)

My Teaser test:


import React from 'react'
import { render, screen } from '../../utils/test-utils'
import { Teaser, Tonieboxes } from './Teaser'

const tonieboxes: Tonieboxes[] = [
  {
    id: 'toniebox-id-1',
    name: 'toniebox-name-1',
    imageUrl: 'toniebox-image-1',
  },
]

describe('Teaser component', () => {
  const welcomeMessage = 'welcome-message'
  test('renders Teaser component when user has tonieboxes', () => {
    render(<Teaser tonieboxes={tonieboxes} />)
    expect(screen.getByTestId(welcomeMessage)).toBeInTheDocument()
  })
})


My Teaser Component:


import React, { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import {
  variables,
  Text,
  Bello,
  media,
  Headline,
  Modal,
} from '@boxine/tonies-ui'
import { Link } from 'react-router-dom'
import { HorizontalScrollList } from '../../components/HorizontalScrollList/index'
import BenjaminBlümchen from '../../assets/01_Teaser_Charakter Benjamin.png'
import BibiAndTinaImg from '../../assets/05_Teaser_Charaktere Bibi&Tina.png'

/* German Images */
import newEpisodesImgDE from '../../assets/03_Teaser_Welcome Audiothek DE.png'
import newTonieBoxTurqouiseImgDE from '../../assets/02_2_Teaser_Toniebox Turquoise DE.png'
import creativeToniesImgDE from '../../assets/04_Teaser_Kreativ-Tonies DE.png'
import registerTonieboxImgDE from '../../assets/02_1_Teaser_Toniebox registrieren DE.png'
/* English Images */
import newEpisodesImg from '../../assets/03_Teaser_Welcome Audiothek EN.png'
import newTonieBoxTurqouiseImg from '../../assets/02_2_Teaser_Toniebox Turquoise EN.png'
import creativeToniesImg from '../../assets/04_Teaser_Kreativ-Tonies EN.png'
import AddTonieboxModalContent from '../tonieboxes-page/components/AddTonieboxModalContent'
import registerTonieboxImg from '../../assets/02_1_Teaser_Toniebox registrieren EN.png'

export interface Tonieboxes {
  id: string
  name: string
  imageUrl: string
}

interface TeaserProps {
  tonieboxes: Tonieboxes[]
}

interface TunesTeaser {
  alt: string
  src: string
  link: string
  noTonieboxes?: boolean
}

const tunesTeasersDE: TunesTeaser[] = [
  {
    alt: 'BenjaminBlümchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  },
  {
    alt: 'Tonieboxen',
    src: newTonieBoxTurqouiseImgDE,
    link: '/tonieboxes',
  },
  {
    alt: '400 Neue Folgen',
    src: newEpisodesImgDE,
    link: '/audio-library',
  },
  {
    alt: 'Kreativ Tonies',
    src: creativeToniesImgDE,
    link: '/creative-tonies',
  },
  {
    alt: 'Bibi und Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  },
]

const tunesTeasersNoTonieboxesDE: TunesTeaser[] = [
  {
    alt: 'BenjaminBlümchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  },
  {
    alt: 'Registriere Deine Toniebox',
    src: registerTonieboxImgDE,
    link: '',
    noTonieboxes: true,
  },
  {
    alt: '400 Neue Folgen',
    src: newEpisodesImgDE,
    link: '/audio-library',
  },
  {
    alt: 'Kreativ Tonies',
    src: creativeToniesImgDE,
    link: '/creative-tonies',
  },
  {
    alt: 'Bibi und Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  },
]

const tunesTeasersEng: TunesTeaser[] = [
  {
    alt: 'Benjamin Bluemchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  },
  {
    alt: 'Tonieboxes',
    src: newTonieBoxTurqouiseImg,
    link: '/tonieboxes',
  },
  {
    alt: '400 New Episodes',
    src: newEpisodesImg,
    link: '/audio-library',
  },
  {
    alt: 'Creative Tonies',
    src: creativeToniesImg,
    link: '/creative-tonies',
  },
  {
    alt: 'Bibi and Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  },
]

const tunesTeasersNoTonieboxesEng: TunesTeaser[] = [
  {
    alt: 'Benjamin Bluemchen',
    src: BenjaminBlümchen,
    link: '/audio-library?filter=beee313f-55b2-40c1-8032-c41057f92e21',
  },
  {
    alt: 'Register Your Toniebox',
    src: registerTonieboxImg,
    link: '',
    noTonieboxes: true,
  },
  {
    alt: '400 New Episodes',
    src: newEpisodesImg,
    link: '/audio-library',
  },
  {
    alt: 'Creative Tonies',
    src: creativeToniesImg,
    link: '/creative-tonies',
  },
  {
    alt: 'Bibi and Tina',
    src: BibiAndTinaImg,
    link: '/audio-library?filter=dacc4edb-ad1d-4ecd-b98c-b4b31983b5f8',
  },
]

const Wrapper = styled.div`
  margin: 1rem 0 0;
`

const StyledLink = styled(Link)`
  display: block;
`

const List = styled.li`
  display: block;
  cursor: pointer;
`

const StyledHeadline = styled(Headline)`
  text-align: center;
`

const StyledText = styled(Text)`
  text-align: center;
  ${media.tablet`
    font-size: 1rem;
  `}
  ${media.laptop`
    font-size: 1.25rem;
  `}
`

const TextWrapper = styled.div`
  position: relative;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 1rem;
  width: 18rem;
    ${media.mobileL`
    width: 21rem;
    `}
    ${media.tablet`
    width: 26rem;
    `}
    ${media.laptop`
    width: 27rem;
    `}
`
const StyledHorizontalScrollList = styled(HorizontalScrollList)`
  ul {
    padding: 0 1rem 0.5rem 0;
  }
`

const ScrollListWrapper = styled.div`
  margin-left: 1rem;
  ${media.laptopL`
    margin-left: 0;
  `}
`

export const TeaserCard = styled.img`
  width: 100%;
  height: 100%;

  border-radius: 1rem;
  box-shadow: 0.25rem 0.25rem 0 0 ${props => props.theme.DirtyWhiteDarker};
  ${media.tablet`
    box-shadow: 0.375rem 0.375rem 0 0 ${props => props.theme.DirtyWhiteDarker};
  `}
  ${media.laptop`
    box-shadow: 0.5rem 0.5rem 0 0 ${props => props.theme.DirtyWhiteDarker};
  `}
`

export function Teaser({ tonieboxes }: TeaserProps) {
  const [columns, setColumns] = useState(3)
  const [toggleTonieboxModal, setToggleTonieboxModal] = useState(false)
  const [allBoxes, setAllBoxes] = useState<Tonieboxes[]>(tonieboxes)
  const [tunesTeasers, setTunesTeasers] = useState<TunesTeaser[]>([])

  const { i18n } = useTranslation()
  const { t } = useTranslation(['home'])

  function toggleModal() {
    setToggleTonieboxModal(!toggleTonieboxModal)
  }

  function tonieboxAdded(toniebox) {
    setAllBoxes([...allBoxes, toniebox])
  }

  useEffect(() => {
    function update() {
      const matchTablet = window.matchMedia(
        `(min-width: ${variables.screenTablet}px)`
      ).matches
      const matchScreenMobileLarge = window.matchMedia(
        `(min-width: ${variables.screenMobileL}px)`
      ).matches

      setColumns(matchTablet ? 2.75 : matchScreenMobileLarge ? 2 : 1.35)
    }

    update()

    function checkAndSetTunesTeasers() {
      if (i18n.language === 'de') {
        if (tonieboxes.length === 0) {
          setTunesTeasers(tunesTeasersNoTonieboxesDE)
        } else {
          setTunesTeasers(tunesTeasersDE)
        }
      } else {
        if (tonieboxes.length === 0) {
          setTunesTeasers(tunesTeasersNoTonieboxesEng)
        } else {
          setTunesTeasers(tunesTeasersEng)
        }
      }
    }

    checkAndSetTunesTeasers()

    window.addEventListener('resize', update)
    return () => window.removeEventListener('resize', update)
  }, [i18n.language, tonieboxes.length])

  return (
    <>
      <Wrapper>
        <TextWrapper>
          <StyledHeadline
            styleTag={columns === 1.25 ? 'h3' : 'h2'}
            dataTestId="welcome-message"
          >
            Werde ein <Bello>Ipsum</Bello> der Tonies
          </StyledHeadline>
          <StyledText>
            Bist du bereit für Hörabenteuer? Entdecke jetzt die ganze Vielfalt
            der Tonies.
          </StyledText>
        </TextWrapper>
        <ScrollListWrapper>
          <StyledHorizontalScrollList columns={columns}>
            {tunesTeasers.map(teaser => {
              return (
                <>
                  {teaser.noTonieboxes ? (
                    <List key={teaser.alt} onClick={toggleModal}>
                      <TeaserCard src={teaser.src} alt={teaser.alt} />
                    </List>
                  ) : (
                    <StyledLink key={teaser.alt} to={teaser.link}>
                      <TeaserCard src={teaser.src} alt={teaser.alt} />
                    </StyledLink>
                  )}
                </>
              )
            })}
          </StyledHorizontalScrollList>
        </ScrollListWrapper>
      </Wrapper>
      <Modal
        headline={t('add-toniebox-modal:AddTonieboxModalTitle')}
        isOpen={toggleTonieboxModal}
        onClose={toggleModal}
      >
        <AddTonieboxModalContent
          onClose={toggleModal}
          onSuccess={tonieboxAdded}
        />
      </Modal>
    </>
  )
}

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Tiger Abrodi
  • 113
  • 1
  • 12
  • 3
    Well the error is pretty self-explanatory. And there's been like a million posts about this. The error is in your `.map` call. The outermost element (in this case a fragment) needs a key. So like this: `{tunesTeasers.map(teaser => { return ( ...` - I am assuming the `src` property is unique between the different teasers. If not, use one of their properties that is. If they don't have one, make one – Jayce444 Sep 30 '20 at 05:45
  • What does your TeaserCard component look like? It looks like you have a unique key for each set of data for the `List` and `StyledLink` component. – Ross Sheppard Sep 30 '20 at 05:48

2 Answers2

0

Likely related to tunesTeasers.map. A react key needs to be on the outer-most element mapped, the Fragment in this case. The teaser link appears to be unique within the set, but if it isn't, you may need to provide a unique id property to the elements to use as a react key.

{tunesTeasers.map(teaser => {
  return (
    <Fragment key={teaser.link}>
      {teaser.noTonieboxes ? (
        <List key={teaser.alt} onClick={toggleModal}>
          <TeaserCard src={teaser.src} alt={teaser.alt} />
        </List>
      ) : (
        <StyledLink key={teaser.alt} to={teaser.link}>
          <TeaserCard src={teaser.src} alt={teaser.alt} />
        </StyledLink>
      )}
    </Fragment>
  )
})}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
0

It's definitely an array key issue, but it seems you have unique alt attributes in each data set (array).

<StyledHorizontalScrollList columns={columns}>
  {tunesTeasers.map(teaser => teaser.noTonieboxes ? (
    <List key={teaser.alt} onClick={toggleModal}>
      <TeaserCard alt={teaser.alt} src={teaser.src} />
    </List>
  ) : (
    <StyledLink key={teaser.alt} to={teaser.link}>
      <TeaserCard alt={teaser.alt} src={teaser.src} />
    </StyledLink>
  )}
</StyledHorizontalScrollList>
Ross Sheppard
  • 850
  • 1
  • 6
  • 14