diff --git a/src/components/ModalsContainer/BriefDescriptionModal/BriefDescriptionContent/index.tsx b/src/components/ModalsContainer/BriefDescriptionModal/BriefDescriptionContent/index.tsx index e0b30baa8..3d746b479 100644 --- a/src/components/ModalsContainer/BriefDescriptionModal/BriefDescriptionContent/index.tsx +++ b/src/components/ModalsContainer/BriefDescriptionModal/BriefDescriptionContent/index.tsx @@ -1,185 +1,185 @@ -import { Button } from '@mui/material' -import clsx from 'clsx' -import { FC, useCallback, useEffect, useRef, useState } from 'react' -import Markdown from 'react-markdown' -import styled from 'styled-components' -import BubbleChartIcon from '~/components/Icons/BubbleChartIcon' -import PauseIcon from '~/components/Icons/PauseIcon' -import SoundIcon from '~/components/Icons/SoundIcon' -import { Flex } from '~/components/common/Flex' -import { Text } from '~/components/common/Text' -import { useAppStore } from '~/stores/useAppStore' -import { useDataStore } from '~/stores/useDataStore' -import { useModal } from '~/stores/useModalStore' -import { useUserStore } from '~/stores/useUserStore' -import { Trending } from '~/types' -import { colors } from '~/utils' - -type Props = { - trend: Trending - onClose: () => void -} - -export const BriefDescriptionContent: FC = ({ trend, onClose }) => { - const [isPlaying, setIsPlaying] = useState(false) - const { close } = useModal('briefDescription') - - const { currentPlayingAudio, setCurrentPlayingAudio } = useAppStore((s) => s) - - const [setBudget] = useUserStore((s) => [s.setBudget]) - const { fetchData, setAbortRequests } = useDataStore((s) => s) - - const audioRef = useRef(null) - - const handleLearnMore = async () => { - handleClose() - await fetchData(setBudget, setAbortRequests, trend.tldr_topic ?? trend.name) - } - - const handleClose = useCallback(() => { - onClose() - close() - }, [onClose, close]) - - const handleToggle = () => { - if (audioRef.current) { - if (isPlaying) { - audioRef.current.pause() - } else { - audioRef.current.play() - } - - setIsPlaying(!isPlaying) - } - } - - const togglePlay = () => { - const isBackgroundAudioPlaying = !currentPlayingAudio?.current?.paused - - if (isBackgroundAudioPlaying) { - currentPlayingAudio?.current?.pause() - setCurrentPlayingAudio(null) - } - - if (currentPlayingAudio?.current?.src !== trend.audio_EN || !isBackgroundAudioPlaying) { - handleToggle() - } - } - - useEffect(() => { - const audioElement = audioRef.current - - const onAudioPlaybackComplete = () => { - setIsPlaying(false) - setCurrentPlayingAudio(null) - } - - if (audioElement) { - audioElement.addEventListener('ended', onAudioPlaybackComplete) - } - - return () => { - if (audioElement) { - audioElement.removeEventListener('ended', onAudioPlaybackComplete) - } - } - }, [setCurrentPlayingAudio]) - - const showPlayBtn = - (currentPlayingAudio?.current?.src === trend.audio_EN && !currentPlayingAudio?.current?.paused) || isPlaying - - return ( - <> - {trend.audio_EN ? ( - <> - - : } - > - {showPlayBtn ? 'Pause' : 'Listen'} - - - }> - Learn More - - - - - - - ) : null} - - {trend.tldr_topic ?? trend.name} - - - {trend.tldr && {trend.tldr}} - - - - - ) -} - -const ScrollableContent = styled.div` - max-height: 310px; - overflow-y: auto; - margin: 8px 0; - padding: 0 20px; -` - -const StyledText = styled(Text)` - font-size: 18px; - font-weight: 400; - font-family: 'Barlow'; - * { - all: revert; - } -` - -const Title = styled(Text)` - font-weight: 600; - font-size: 20px; - padding: 0 20px; -` - -const StyledAudio = styled.audio` - display: none; -` - -const StyleButton = styled(Button)` - && { - &.default { - font-size: 13px; - font-weight: 500; - font-family: Barlow; - padding: 12px, 16px, 12px, 10px; - color: ${colors.white}; - - &:hover { - color: ${colors.GRAY3}; - } - - &.play { - color: ${colors.BG3}; - background-color: ${colors.white}; - } - } - } -` - -const StyledHeader = styled(Flex)` - top: 0px; - position: absolute; - border-radius: 16px 16px 0px 0px; - padding: 0px 12px; - width: 100%; - height: 60px; - display: flex; - flex-direction: row; - align-items: center; - background-color: ${colors.BG3}; - gap: 10px; -` +import { Button } from '@mui/material' +import clsx from 'clsx' +import { FC, useCallback, useEffect, useRef, useState } from 'react' +import Markdown from 'react-markdown' +import styled from 'styled-components' +import BubbleChartIcon from '~/components/Icons/BubbleChartIcon' +import PauseIcon from '~/components/Icons/PauseIcon' +import SoundIcon from '~/components/Icons/SoundIcon' +import { Flex } from '~/components/common/Flex' +import { Text } from '~/components/common/Text' +import { useAppStore } from '~/stores/useAppStore' +import { useDataStore } from '~/stores/useDataStore' +import { useModal } from '~/stores/useModalStore' +import { useUserStore } from '~/stores/useUserStore' +import { Trending } from '~/types' +import { colors } from '~/utils' + +type Props = { + trend: Trending + onClose: () => void +} + +export const BriefDescriptionContent: FC = ({ trend, onClose }) => { + const [isPlaying, setIsPlaying] = useState(false) + const { close } = useModal('briefDescription') + + const { currentPlayingAudio, setCurrentPlayingAudio } = useAppStore((s) => s) + + const [setBudget] = useUserStore((s) => [s.setBudget]) + const { fetchData, setAbortRequests } = useDataStore((s) => s) + + const audioRef = useRef(null) + + const handleLearnMore = async () => { + handleClose() + await fetchData(setBudget, setAbortRequests, trend.tldr_topic ?? trend.name) + } + + const handleClose = useCallback(() => { + onClose() + close() + }, [onClose, close]) + + const handleToggle = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause() + } else { + audioRef.current.play() + } + + setIsPlaying(!isPlaying) + } + } + + const togglePlay = () => { + const isBackgroundAudioPlaying = !currentPlayingAudio?.current?.paused + + if (isBackgroundAudioPlaying) { + currentPlayingAudio?.current?.pause() + setCurrentPlayingAudio(null) + } + + if (currentPlayingAudio?.current?.src !== trend.audio_EN || !isBackgroundAudioPlaying) { + handleToggle() + } + } + + useEffect(() => { + const audioElement = audioRef.current + + const onAudioPlaybackComplete = () => { + setIsPlaying(false) + setCurrentPlayingAudio(null) + } + + if (audioElement) { + audioElement.addEventListener('ended', onAudioPlaybackComplete) + } + + return () => { + if (audioElement) { + audioElement.removeEventListener('ended', onAudioPlaybackComplete) + } + } + }, [setCurrentPlayingAudio]) + + const showPlayBtn = + (currentPlayingAudio?.current?.src === trend.audio_EN && !currentPlayingAudio?.current?.paused) || isPlaying + + return ( + <> + {trend.audio_EN ? ( + <> + + : } + > + {showPlayBtn ? 'Pause' : 'Listen'} + + + }> + Learn More + + + + + + + ) : null} + + {trend.tldr_topic ?? trend.name} + + + {trend.tldr && {trend.tldr}} + + + + + ) +} + +const ScrollableContent = styled.div` + max-height: 310px; + overflow-y: auto; + margin: 8px 0; + padding: 0 20px; +` + +const StyledText = styled(Text)` + font-size: 18px; + font-weight: 400; + font-family: 'Barlow'; + * { + all: revert; + } +` + +const Title = styled(Text)` + font-weight: 600; + font-size: 20px; + padding: 0 20px; +` + +const StyledAudio = styled.audio` + display: none; +` + +const StyleButton = styled(Button)` + && { + &.default { + font-size: 13px; + font-weight: 500; + font-family: Barlow; + padding: 12px, 16px, 12px, 10px; + color: ${colors.white}; + + &:hover { + color: ${colors.GRAY3}; + } + + &.play { + color: ${colors.BG3}; + background-color: ${colors.white}; + } + } + } +` + +const StyledHeader = styled(Flex)` + top: 0px; + position: absolute; + border-radius: 16px 16px 0px 0px; + padding: 0px 12px; + width: 100%; + height: 60px; + display: flex; + flex-direction: row; + align-items: center; + background-color: ${colors.BG3}; + gap: 10px; +` diff --git a/src/components/ModalsContainer/BriefDescriptionModal/__tests__/index.tsx b/src/components/ModalsContainer/BriefDescriptionModal/__tests__/index.tsx index c85a99898..8fb5fc519 100644 --- a/src/components/ModalsContainer/BriefDescriptionModal/__tests__/index.tsx +++ b/src/components/ModalsContainer/BriefDescriptionModal/__tests__/index.tsx @@ -1,137 +1,137 @@ -import '@testing-library/jest-dom' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' -import React from 'react' -import { BriefDescription, useBriefDescriptionStore } from '~/components/ModalsContainer/BriefDescriptionModal' -import { useAppStore } from '~/stores/useAppStore' - -window.React = React - -jest.mock('~/stores/useModalStore', () => ({ - ...jest.requireActual('~/stores/useModalStore'), - useModal: (id: string) => ({ - close: jest.fn(), - open: jest.fn(), - visible: id === 'briefDescription', - }), -})) - -jest.mock('~/stores/useAppStore', () => ({ - useAppStore: jest.fn(), -})) - -jest.mock('~/components/Icons/PauseIcon', () => jest.fn(() =>
)) -jest.mock('~/components/Icons/SoundIcon', () => jest.fn(() =>
)) - -const useAppStoreMock = useAppStore as jest.MockedFunction - -describe('BriefDescription Component Tests', () => { - const trendMock = { - count: 1, - name: 'trend', - audio_EN: 'fake-audio-url', - tldr_topic: 'Test Topic', - tldr: '', - } - - beforeEach(() => { - useAppStoreMock.mockReturnValue({ - currentPlayingAudio: { current: null }, - setCurrentPlayingAudio: jest.fn(), - }) - - useBriefDescriptionStore.setState({ trend: trendMock }) - }) - - afterEach(() => { - jest.clearAllMocks() - useBriefDescriptionStore.setState({ trend: null }) - }) - - it('renders title, audio button, and tldr', () => { - render() - - expect(screen.getByText('Test Topic')).toBeInTheDocument() - expect(screen.getByText('Listen')).toBeInTheDocument() - }) - - it('toggles play/pause on audio button click', () => { - render() - - const handleClick = jest.fn() - const audioButton = screen.getByText('Listen').closest('button') as HTMLButtonElement - - fireEvent.click(audioButton) - - setTimeout(() => { - expect(handleClick).toHaveBeenCalled() - }, 0) - }) - - it('ensures that listen Icon only display if the audio is not currently playing in the background', () => { - const { getByTestId, getByText } = render() - - expect(getByTestId('listen-icon')).toBeInTheDocument() - expect(getByText('Listen')).toBeInTheDocument() - }) - - it('ensures that pause icon only displays if the audio is playing in the background', () => { - const mockCurrentPlayingAudio = { current: { src: trendMock.audio_EN, paused: false } } - - useAppStoreMock.mockReturnValue({ currentPlayingAudio: mockCurrentPlayingAudio, setCurrentPlayingAudio: jest.fn() }) - - const { getByTestId, getByText } = render() - - expect(getByTestId('pause-icon')).toBeInTheDocument() - expect(getByText('Pause')).toBeInTheDocument() - }) - - it('ensures that clicking play in TLDR modal stops current background audio and starts new audio', async () => { - const setCurrentPlayingAudioMock = jest.fn() - - useAppStoreMock.mockReturnValue({ - currentPlayingAudio: { current: { src: 'random-audio-file', paused: false, pause: jest.fn() } }, - setCurrentPlayingAudio: setCurrentPlayingAudioMock, - }) - - const { getByRole, container } = render() - - const listenButton = getByRole('button', { name: /Listen/i }) - - expect(listenButton).toBeInTheDocument() - - fireEvent.click(listenButton) - - waitFor(async () => { - expect(setCurrentPlayingAudioMock).toHaveBeenCalledWith(null) - expect(container.querySelector('audio')?.paused).toBe(true) - }) - }) - - it('should clear trend and close modal when closing', () => { - jest.spyOn(useBriefDescriptionStore.getState(), 'setTrend') - - render() - - const { setTrend } = useBriefDescriptionStore.getState() - - setTrend(null) - - expect(useBriefDescriptionStore.getState().trend).toBeNull() - }) - - it('does not close the modal on Command+Control+3 or Command+Control+4', () => { - const { getByTestId } = render() - - waitFor(() => { - const modal = getByTestId('brief-description-modal') - - // With Command+Control+3 - fireEvent.keyDown(window, { key: '3', ctrlKey: true, metaKey: true }) - expect(modal).toBeInTheDocument() - - // With Command+Control+4 - fireEvent.keyDown(window, { key: '4', ctrlKey: true, metaKey: true }) - expect(modal).toBeInTheDocument() - }) - }) -}) +import '@testing-library/jest-dom' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import React from 'react' +import { BriefDescription, useBriefDescriptionStore } from '~/components/ModalsContainer/BriefDescriptionModal' +import { useAppStore } from '~/stores/useAppStore' + +window.React = React + +jest.mock('~/stores/useModalStore', () => ({ + ...jest.requireActual('~/stores/useModalStore'), + useModal: (id: string) => ({ + close: jest.fn(), + open: jest.fn(), + visible: id === 'briefDescription', + }), +})) + +jest.mock('~/stores/useAppStore', () => ({ + useAppStore: jest.fn(), +})) + +jest.mock('~/components/Icons/PauseIcon', () => jest.fn(() =>
)) +jest.mock('~/components/Icons/SoundIcon', () => jest.fn(() =>
)) + +const useAppStoreMock = useAppStore as jest.MockedFunction + +describe('BriefDescription Component Tests', () => { + const trendMock = { + count: 1, + name: 'trend', + audio_EN: 'fake-audio-url', + tldr_topic: 'Test Topic', + tldr: '', + } + + beforeEach(() => { + useAppStoreMock.mockReturnValue({ + currentPlayingAudio: { current: null }, + setCurrentPlayingAudio: jest.fn(), + }) + + useBriefDescriptionStore.setState({ trend: trendMock }) + }) + + afterEach(() => { + jest.clearAllMocks() + useBriefDescriptionStore.setState({ trend: null }) + }) + + it('renders title, audio button, and tldr', () => { + render() + + expect(screen.getByText('Test Topic')).toBeInTheDocument() + expect(screen.getByText('Listen')).toBeInTheDocument() + }) + + it('toggles play/pause on audio button click', () => { + render() + + const handleClick = jest.fn() + const audioButton = screen.getByText('Listen').closest('button') as HTMLButtonElement + + fireEvent.click(audioButton) + + setTimeout(() => { + expect(handleClick).toHaveBeenCalled() + }, 0) + }) + + it('ensures that listen Icon only display if the audio is not currently playing in the background', () => { + const { getByTestId, getByText } = render() + + expect(getByTestId('listen-icon')).toBeInTheDocument() + expect(getByText('Listen')).toBeInTheDocument() + }) + + it('ensures that pause icon only displays if the audio is playing in the background', () => { + const mockCurrentPlayingAudio = { current: { src: trendMock.audio_EN, paused: false } } + + useAppStoreMock.mockReturnValue({ currentPlayingAudio: mockCurrentPlayingAudio, setCurrentPlayingAudio: jest.fn() }) + + const { getByTestId, getByText } = render() + + expect(getByTestId('pause-icon')).toBeInTheDocument() + expect(getByText('Pause')).toBeInTheDocument() + }) + + it('ensures that clicking play in TLDR modal stops current background audio and starts new audio', async () => { + const setCurrentPlayingAudioMock = jest.fn() + + useAppStoreMock.mockReturnValue({ + currentPlayingAudio: { current: { src: 'random-audio-file', paused: false, pause: jest.fn() } }, + setCurrentPlayingAudio: setCurrentPlayingAudioMock, + }) + + const { getByRole, container } = render() + + const listenButton = getByRole('button', { name: /Listen/i }) + + expect(listenButton).toBeInTheDocument() + + fireEvent.click(listenButton) + + waitFor(async () => { + expect(setCurrentPlayingAudioMock).toHaveBeenCalledWith(null) + expect(container.querySelector('audio')?.paused).toBe(true) + }) + }) + + it('should clear trend and close modal when closing', () => { + jest.spyOn(useBriefDescriptionStore.getState(), 'setTrend') + + render() + + const { setTrend } = useBriefDescriptionStore.getState() + + setTrend(null) + + expect(useBriefDescriptionStore.getState().trend).toBeNull() + }) + + it('does not close the modal on Command+Control+3 or Command+Control+4', () => { + const { getByTestId } = render() + + waitFor(() => { + const modal = getByTestId('brief-description-modal') + + // With Command+Control+3 + fireEvent.keyDown(window, { key: '3', ctrlKey: true, metaKey: true }) + expect(modal).toBeInTheDocument() + + // With Command+Control+4 + fireEvent.keyDown(window, { key: '4', ctrlKey: true, metaKey: true }) + expect(modal).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/ModalsContainer/BriefDescriptionModal/index.tsx b/src/components/ModalsContainer/BriefDescriptionModal/index.tsx index 858a5a818..a1a8622fb 100644 --- a/src/components/ModalsContainer/BriefDescriptionModal/index.tsx +++ b/src/components/ModalsContainer/BriefDescriptionModal/index.tsx @@ -1,43 +1,43 @@ -import { FC } from 'react' -import { create } from 'zustand' -import { BaseModal } from '~/components/Modal' -import { useModal } from '~/stores/useModalStore' -import { Trending } from '~/types' -import { BriefDescriptionContent } from './BriefDescriptionContent' - -type BriefDescriptionStore = { - trend: Trending | null - setTrend: (trend: Trending | null) => void -} - -export const useBriefDescriptionStore = create((set) => ({ - trend: null, - setTrend: (trend) => set({ trend }), -})) - -export const BriefDescription: FC = () => { - const { close } = useModal('briefDescription') - const { trend, setTrend } = useBriefDescriptionStore() - - const handleClose = () => { - setTrend(null) - close() - } - - if (!trend) { - return null - } - - return ( - - - - ) -} +import { FC } from 'react' +import { create } from 'zustand' +import { BaseModal } from '~/components/Modal' +import { useModal } from '~/stores/useModalStore' +import { Trending } from '~/types' +import { BriefDescriptionContent } from './BriefDescriptionContent' + +type BriefDescriptionStore = { + trend: Trending | null + setTrend: (trend: Trending | null) => void +} + +export const useBriefDescriptionStore = create((set) => ({ + trend: null, + setTrend: (trend) => set({ trend }), +})) + +export const BriefDescription: FC = () => { + const { close } = useModal('briefDescription') + const { trend, setTrend } = useBriefDescriptionStore() + + const handleClose = () => { + setTrend(null) + close() + } + + if (!trend) { + return null + } + + return ( + + + + ) +}