Skip to content

Commit

Permalink
(PC-34395) feat(ChronicleCard): display button when the chronicle is …
Browse files Browse the repository at this point in the history
…too long
  • Loading branch information
yleclercq-pass committed Feb 12, 2025
1 parent 833ba2b commit 81f883a
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react'
import { View } from 'react-native'

import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap'
import { act, render, screen, userEvent } from 'tests/utils'
import { ButtonTertiaryBlack } from 'ui/components/buttons/ButtonTertiaryBlack'

import { ChronicleCard } from './ChronicleCard'

const mockOnLayoutWithButton = {
nativeEvent: {
layout: {
height: 157,
},
},
}

const mockOnLayoutWithoutButton = {
nativeEvent: {
layout: {
height: 30,
},
},
}

const user = userEvent.setup()
const mockOnSeeMoreButtonPress = jest.fn()
jest.useFakeTimers()

describe('ChronicleCard (Mobile)', () => {
it('should render the ChronicleCard component with correct title', () => {
render(
<ChronicleCard {...chroniclesSnap[0]}>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

expect(screen.getByText(chroniclesSnap[0].title)).toBeOnTheScreen()
})

it('should display the "Voir plus" button if content overflows', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]}>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithButton)
})

expect(screen.getByText('Voir plus')).toBeOnTheScreen()
})

it('should not display the "Voir plus" button', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]}>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithoutButton)
})

expect(screen.queryByText('Voir plus')).not.toBeOnTheScreen()
})

it('should call onSeeMoreButtonPress when "Voir plus" is clicked', async () => {
render(
<ChronicleCard {...chroniclesSnap[0]}>
<View>
<ButtonTertiaryBlack
wording="Voir plus"
onPress={() => mockOnSeeMoreButtonPress(chroniclesSnap[0].id)}
/>
</View>
</ChronicleCard>
)

const description = screen.getByTestId('description')

await act(async () => {
description.props.onLayout(mockOnLayoutWithButton)
})

await user.press(screen.getByText('Voir plus'))

expect(mockOnSeeMoreButtonPress).toHaveBeenCalledTimes(1)
expect(mockOnSeeMoreButtonPress).toHaveBeenCalledWith(chroniclesSnap[0].id)
})
})
50 changes: 45 additions & 5 deletions src/features/chronicle/components/ChronicleCard/ChronicleCard.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { FunctionComponent, PropsWithChildren } from 'react'
import styled from 'styled-components/native'
import React, { FunctionComponent, PropsWithChildren, useState } from 'react'
import { LayoutChangeEvent, Platform } from 'react-native'
import styled, { useTheme } from 'styled-components/native'

import { ChronicleCardData } from 'features/chronicle/type'
import { InfoHeader } from 'ui/components/InfoHeader/InfoHeader'
import { Separator } from 'ui/components/Separator'
import { ViewGap } from 'ui/components/ViewGap/ViewGap'
import { BookClubCertification } from 'ui/svg/BookClubCertification'
import { TypoDS, getShadow, getSpacing } from 'ui/theme'
import { REM_TO_PX } from 'ui/theme/constants'

const CHRONICLE_THUMBNAIL_SIZE = getSpacing(14)

Expand All @@ -16,6 +18,9 @@ type Props = PropsWithChildren<
}
>

const MAX_LINES = 3
const CHRONICLE_CARD_HEIGHT = 220

export const ChronicleCard: FunctionComponent<Props> = ({
id,
title,
Expand All @@ -25,6 +30,29 @@ export const ChronicleCard: FunctionComponent<Props> = ({
cardWidth,
children,
}) => {
const theme = useTheme()

const [currentNumberOfLines, setCurrentNumberOfLines] = useState<number | undefined>(undefined)
const [shouldDisplayButton, setShouldDisplayButton] = useState(false)

// height depending on the platform
const DEFAULT_HEIGHT_WEB =
parseFloat(theme.designSystem.typography.bodyAccentS.lineHeight) * MAX_LINES * REM_TO_PX
const DEFAULT_HEIGHT_MOBILE =
parseFloat(theme.designSystem.typography.bodyAccentS.lineHeight) * MAX_LINES
const getDefaultHeight = Platform.OS === 'web' ? DEFAULT_HEIGHT_WEB : DEFAULT_HEIGHT_MOBILE

const handleOnLayout = (event: LayoutChangeEvent) => {
// We use Math.floor to avoid floating-point precision issues when comparing heights
const actualHeight = Math.floor(event.nativeEvent.layout.height)
const expectedMaxHeight = Math.floor(getDefaultHeight)

if (actualHeight > expectedMaxHeight) {
setShouldDisplayButton(true)
setCurrentNumberOfLines(3)
}
}

return (
<Container gap={3} testID={`chronicle-card-${id.toString()}`} width={cardWidth}>
<InfoHeader
Expand All @@ -34,10 +62,17 @@ export const ChronicleCard: FunctionComponent<Props> = ({
thumbnailComponent={<BookClubCertification />}
/>
<Separator.Horizontal />
<Description>{description}</Description>
<DescriptionContainer defaultHeight={getDefaultHeight}>
<Description
testID="description"
onLayout={handleOnLayout}
numberOfLines={currentNumberOfLines}>
{description}
</Description>
</DescriptionContainer>
<BottomCardContainer>
<PublicationDate>{date}</PublicationDate>
{children}
{shouldDisplayButton && children}
</BottomCardContainer>
</Container>
)
Expand All @@ -49,7 +84,7 @@ const Container = styled(ViewGap)<{ width?: number }>(({ theme, width }) => ({
border: 1,
borderColor: theme.colors.greyMedium,
...(width === undefined ? undefined : { width }),

height: CHRONICLE_CARD_HEIGHT,
backgroundColor: theme.colors.white,
...getShadow({
shadowOffset: { width: 0, height: getSpacing(1) },
Expand All @@ -59,6 +94,11 @@ const Container = styled(ViewGap)<{ width?: number }>(({ theme, width }) => ({
}),
}))

const DescriptionContainer = styled.View<{ defaultHeight: number }>(({ defaultHeight }) => ({
maxHeight: MAX_LINES * defaultHeight,
overflow: 'hidden',
}))

const Description = styled(TypoDS.BodyAccentS)(({ theme }) => ({
color: theme.colors.greyDark,
flexGrow: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@ import { ReactTestInstance } from 'react-test-renderer'

import { CHRONICLE_CARD_WIDTH } from 'features/chronicle/constant'
import { chroniclesSnap } from 'features/chronicle/fixtures/chroniclesSnap'
import { render, screen, userEvent } from 'tests/utils'
import { act, render, screen, userEvent } from 'tests/utils'

import { ChronicleCardListBase } from './ChronicleCardListBase'

const mockOnLayoutWithButton = {
nativeEvent: {
layout: {
height: 157,
},
},
}

const user = userEvent.setup()

jest.useFakeTimers()
Expand Down Expand Up @@ -40,7 +48,7 @@ describe('ChronicleCardListBase', () => {
expect(screen.getByText('La Nature Sauvage')).toBeOnTheScreen()
})

it('should display "Voir plus" button on all cards when onPressSeeMoreButton defined', () => {
it('should display "Voir plus" button on all cards when onPressSeeMoreButton defined', async () => {
render(
<ChronicleCardListBase
data={chroniclesSnap}
Expand All @@ -51,7 +59,13 @@ describe('ChronicleCardListBase', () => {
/>
)

expect(screen.getAllByText('Voir plus')).toHaveLength(10)
const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

expect(screen.getAllByText('Voir plus')).toHaveLength(1)
})

it('should not display "Voir plus" button on all cards when onSeeMoreButtonPress not defined', () => {
Expand Down Expand Up @@ -79,10 +93,16 @@ describe('ChronicleCardListBase', () => {
/>
)

const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

const seeMoreButtons = screen.getAllByText('Voir plus')

// Using as because links is never undefined and the typing is not correct
await user.press(seeMoreButtons[2] as ReactTestInstance)
await user.press(seeMoreButtons[0] as ReactTestInstance)

expect(mockOnSeeMoreButtonPress).toHaveBeenCalledTimes(1)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ const nativeEventBottom = {
contentSize: { height: 1900 },
},
}

const mockOnLayoutWithButton = {
nativeEvent: {
layout: {
height: 157,
},
},
}

const BATCH_TRIGGER_DELAY_IN_MS = 5000

jest.useFakeTimers()
Expand Down Expand Up @@ -656,14 +665,20 @@ describe('<OfferContent />', () => {
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.LIVRE_PAPIER },
})

const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

const seeMoreButtons = screen.getAllByText('Voir plus')

// Using as because links is never undefined and the typing is not correct
await user.press(seeMoreButtons[2] as ReactTestInstance)
await user.press(seeMoreButtons[0] as ReactTestInstance)

expect(mockNavigate).toHaveBeenNthCalledWith(1, 'Chronicles', {
offerId: 116656,
chronicleId: 3,
chronicleId: 1,
from: 'chronicles',
})
})
Expand All @@ -673,14 +688,20 @@ describe('<OfferContent />', () => {
offer: { ...offerResponseSnap, subcategoryId: SubcategoryIdEnum.LIVRE_PAPIER },
})

const descriptions = screen.getAllByTestId('description')

await act(async () => {
descriptions[0]?.props.onLayout(mockOnLayoutWithButton)
})

const seeMoreButtons = screen.getAllByText('Voir plus')

// Using as because links is never undefined and the typing is not correct
await user.press(seeMoreButtons[2] as ReactTestInstance)
await user.press(seeMoreButtons[0] as ReactTestInstance)

expect(analytics.logConsultChronicle).toHaveBeenNthCalledWith(1, {
offerId: 116656,
chronicleId: 3,
chronicleId: 1,
})
})
})
Expand Down

0 comments on commit 81f883a

Please sign in to comment.