diff --git a/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.stories.tsx b/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.stories.tsx index 2c6a950208b..fea5c2ca72b 100644 --- a/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.stories.tsx +++ b/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.stories.tsx @@ -69,7 +69,7 @@ type StoryData = HeadToHeadV2Data & { }; interface ComponentProps { - data: StoryData; + initialSportData: StoryData; isConciseView?: boolean; shouldShowActions?: boolean; maximumContainerScoreDigits?: number; @@ -80,7 +80,7 @@ const baseData = fixtureData.data.sportDataEventContent .sportDataEvent as unknown as StoryData; const Component = ({ - data, + initialSportData, isConciseView = false, shouldShowActions = true, maximumContainerScoreDigits, @@ -88,7 +88,7 @@ const Component = ({ }: ComponentProps) => { return ( ; +export const Default = () => ; export const ConciseView = () => ( - + ); export const CancelledEvent = HeadToHeadV2Component.bind({}); @@ -115,6 +119,7 @@ export const EventWithOnwardJourneyHoverConcise = // @ts-expect-error - PS copy and paste CancelledEvent.args = { + urn: 'urn:bbc:sportsdata:football:event:s-3y91hnyfjh24yxjhm77a7hy50', home: 'Fulham', away: 'Liverpool', baseData: cancelledEventData, diff --git a/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.tsx b/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.tsx index 16393a621b1..a00f073fe42 100644 --- a/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.tsx +++ b/src/app/components-webcore/SportDataHeader/head-to-head-v2/head-to-head-v2.tsx @@ -1,3 +1,5 @@ +import useSportDataPolling from '#app/hooks/useSportDataPolling'; +import useToggle from '#app/hooks/useToggle'; import Footer from './components/footer'; import HeadToHeadHeader from './components/head-to-head-header'; import { HeadToHeadBanner } from './components/head-to-head-banner'; @@ -7,21 +9,30 @@ import { HeadToHeadV2Data } from './types'; import styles from './index.styles'; export const HeadToHeadV2 = ({ - data, + initialSportData, isConciseView, shouldShowActions, maximumContainerScoreDigits, teamBadgePlaceholderFallbackType = 'badge', + isSportDataLive = false, }: { - data: HeadToHeadV2Data; + initialSportData: HeadToHeadV2Data; isConciseView?: boolean; shouldShowActions?: boolean; maximumContainerScoreDigits?: number; teamBadgePlaceholderFallbackType?: 'badge' | 'flag'; + isSportDataLive?: boolean; }) => { + const { enabled: sportHeaderPollEnabled } = useToggle('sportDataPolling'); + + const { currentSportData } = useSportDataPolling( + initialSportData, + Boolean(sportHeaderPollEnabled) && isSportDataLive, + ); + const hasActions = - (data?.home?.actions?.length ?? 0) > 0 || - (data?.away?.actions?.length ?? 0) > 0; + (currentSportData?.home?.actions?.length ?? 0) > 0 || + (currentSportData?.away?.actions?.length ?? 0) > 0; // TODO: Re-enable badge visibility logic once we have the necessary badge mappings in place const shouldHideBadges = true; @@ -32,26 +43,30 @@ export const HeadToHeadV2 = ({
{!isConciseView && ( )} - {hasActions && shouldShowActions && } - {!isConciseView && } + {hasActions && shouldShowActions && ( + + )} + {!isConciseView && } {!isConciseView && (
)}
diff --git a/src/app/components-webcore/SportDataHeader/head-to-head-v2/storybook/helpers/base-component.tsx b/src/app/components-webcore/SportDataHeader/head-to-head-v2/storybook/helpers/base-component.tsx index 44d12521ff9..d82239d4d7b 100644 --- a/src/app/components-webcore/SportDataHeader/head-to-head-v2/storybook/helpers/base-component.tsx +++ b/src/app/components-webcore/SportDataHeader/head-to-head-v2/storybook/helpers/base-component.tsx @@ -96,7 +96,7 @@ export const HeadToHeadV2ConciseComponent = (args: StoryArgs) => { return ( @@ -153,7 +153,7 @@ export const HeadToHeadV2Component = (args: StoryArgs) => { return ( diff --git a/src/app/components-webcore/SportDataHeader/head-to-head-v2/tests/index.test.tsx b/src/app/components-webcore/SportDataHeader/head-to-head-v2/tests/index.test.tsx index 056dbfbf784..3e5a34f6ac3 100644 --- a/src/app/components-webcore/SportDataHeader/head-to-head-v2/tests/index.test.tsx +++ b/src/app/components-webcore/SportDataHeader/head-to-head-v2/tests/index.test.tsx @@ -29,6 +29,13 @@ import { import HeadToHead from '../head-to-head-v2'; import type { HeadToHeadV2Data } from '../types'; +jest.mock('#app/hooks/useSportDataPolling', () => ({ + __esModule: true, + default: jest.fn(initialSportData => ({ + currentSportData: initialSportData, + })), +})); + interface RenderOptions { data: HeadToHeadV2Data; isConciseView?: boolean; @@ -42,7 +49,7 @@ const renderHeadToHead = ({ }: RenderOptions) => render( , diff --git a/src/app/components-webcore/SportDataHeader/head-to-head-v2/types.ts b/src/app/components-webcore/SportDataHeader/head-to-head-v2/types.ts index 9efb743e27f..374041add69 100644 --- a/src/app/components-webcore/SportDataHeader/head-to-head-v2/types.ts +++ b/src/app/components-webcore/SportDataHeader/head-to-head-v2/types.ts @@ -116,6 +116,10 @@ export type Team = { }; export type HeadToHeadV2Data = { + /** + * The event's unique urn. + */ + urn: string; /** * The event's unique id. */ @@ -206,7 +210,7 @@ export type BadgeSize = | { small?: number; medium?: number; large?: number }; export interface HeadToHeadV2Props { - data: HeadToHeadV2Data; + initialSportData: HeadToHeadV2Data; isConciseView: boolean; shouldShowActions?: boolean; /** diff --git a/src/app/hooks/useSportDataPolling/fixture/fixtureSportData.js b/src/app/hooks/useSportDataPolling/fixture/fixtureSportData.js new file mode 100644 index 00000000000..9a02dec0129 --- /dev/null +++ b/src/app/hooks/useSportDataPolling/fixture/fixtureSportData.js @@ -0,0 +1,140 @@ +export default { + data: { + title: 'PSG edge out Bayern in record nine-goal semi-final first leg', + live: false, + startDateTime: '2026-04-28T17:30:00.000Z', + countingServiceDataAverage: 0, + sportDataEvent: { + urn: 'urn:bbc:sportsdata:football:event:s-3y91hnyfjh24yxjhm77a7hy50', + home: { + id: 'ej5er0oyngdw138yuumwqbyqt', + fullName: 'Bologna', + shortName: 'Bologna', + urn: 'urn:bbc:sportsdata:football:team:bologna', + runningScores: { + halftime: '0', + fulltime: '1', + aggregate: '1', + }, + scoreUnconfirmed: '1', + actions: [ + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-2bmeynv0dhsc8sjfuaprkexre', + playerName: 'J. Rowe', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "90'", + accessible: '90 minutes', + }, + }, + ], + }, + ], + score: '1', + }, + away: { + id: 'b496gs285it6bheuikox6z9mj', + fullName: 'Aston Villa', + shortName: 'Aston Villa', + urn: 'urn:bbc:sportsdata:football:team:aston-villa', + runningScores: { + halftime: '1', + fulltime: '3', + aggregate: '3', + }, + scoreUnconfirmed: '3', + actions: [ + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-8qys6qtdwgsycxducl062zld5', + playerName: 'E. Konsa', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "44'", + accessible: '44 minutes', + }, + }, + ], + }, + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-5m0j33eoa5c8pqlr0tdf7undh', + playerName: 'O. Watkins', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "51'", + accessible: '51 minutes', + }, + }, + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "90'+4", + accessible: '90 minutes plus 4', + }, + }, + ], + }, + ], + score: '3', + }, + time: { + accessibleTime: '20:00', + displayTimeUK: '20:00', + timeCertainty: true, + }, + date: 'Thu 9 Apr 2026', + tournament: { + id: '4c1nfi2j1m731hcay25fcgndq', + name: 'UEFA Europa League', + disambiguatedName: 'UEFA Europa League', + urn: 'urn:bbc:sportsdata:football:tournament:europa-league', + thingsGuid: '2afbdda7-71d4-544d-bcc6-d9ff50314b2a', + }, + stage: { + id: '7wxuj38kqm8bz3cmi15vu4w7o', + name: 'Quarter-finals', + urn: '', + }, + multiLeg: { + leg: 1, + relatedMatchId: 's-9ur6e6w5f4ahyxph7ef4rks2c', + }, + period: 'ft', + venue: { + id: '2nrn0y55nz9ee7p9adzbb7fta', + urn: 'urn:bbc:sportsdata:football:venue:s-2nrn0y55nz9ee7p9adzbb7fta', + name: "Stadio Renato Dall'Ara", + shortName: "Stadio Renato Dall'Ara", + }, + attendance: { value: 31142 }, + status: 'PostEvent', + periodLabel: { value: 'FT', accessible: 'Full time' }, + winner: 'away', + tournamentDescriptionLabel: 'UEFA Europa League - Quarter-finals', + groupedActions: [ + { + groupName: { fullName: 'Assists', shortName: 'Assists' }, + homeTeamActions: ["J. Lucumí (90')"], + awayTeamActions: ["Y. Tielemans (44', 90'+4)", "E. Buendía (51')"], + }, + ], + accessibleEventSummary: 'Bologna 1 , Aston Villa 3 at Full time', + sportDiscipline: 'football', + }, + }, +}; diff --git a/src/app/hooks/useSportDataPolling/fixture/fixtureSportDataUpdate.js b/src/app/hooks/useSportDataPolling/fixture/fixtureSportDataUpdate.js new file mode 100644 index 00000000000..158b140b7ef --- /dev/null +++ b/src/app/hooks/useSportDataPolling/fixture/fixtureSportDataUpdate.js @@ -0,0 +1,140 @@ +export default { + data: { + title: 'PSG edge out Bayern in record nine-goal semi-final first leg', + live: false, + startDateTime: '2026-04-28T17:30:00.000Z', + countingServiceDataAverage: 0, + sportDataEvent: { + urn: 'urn:bbc:sportsdata:football:event:s-3y91hnyfjh24yxjhm77a7hy50', + home: { + id: 'ej5er0oyngdw138yuumwqbyqt', + fullName: 'Bologna', + shortName: 'Bologna', + urn: 'urn:bbc:sportsdata:football:team:bologna', + runningScores: { + halftime: '0', + fulltime: '2', + aggregate: '2', + }, + scoreUnconfirmed: '2', + actions: [ + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-2bmeynv0dhsc8sjfuaprkexre', + playerName: 'J. Rowe', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "90'", + accessible: '90 minutes', + }, + }, + ], + }, + ], + score: '2', + }, + away: { + id: 'b496gs285it6bheuikox6z9mj', + fullName: 'Aston Villa', + shortName: 'Aston Villa', + urn: 'urn:bbc:sportsdata:football:team:aston-villa', + runningScores: { + halftime: '1', + fulltime: '3', + aggregate: '3', + }, + scoreUnconfirmed: '3', + actions: [ + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-8qys6qtdwgsycxducl062zld5', + playerName: 'E. Konsa', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "44'", + accessible: '44 minutes', + }, + }, + ], + }, + { + playerUrn: + 'urn:bbc:sportsdata:football:player:s-5m0j33eoa5c8pqlr0tdf7undh', + playerName: 'O. Watkins', + actionType: 'goal', + actions: [ + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "51'", + accessible: '51 minutes', + }, + }, + { + type: 'Goal', + typeLabel: { value: 'Goal', accessible: 'Goal' }, + timeLabel: { + value: "90'+4", + accessible: '90 minutes plus 4', + }, + }, + ], + }, + ], + score: '3', + }, + time: { + accessibleTime: '20:00', + displayTimeUK: '20:00', + timeCertainty: true, + }, + date: 'Thu 9 Apr 2026', + tournament: { + id: '4c1nfi2j1m731hcay25fcgndq', + name: 'UEFA Europa League', + disambiguatedName: 'UEFA Europa League', + urn: 'urn:bbc:sportsdata:football:tournament:europa-league', + thingsGuid: '2afbdda7-71d4-544d-bcc6-d9ff50314b2a', + }, + stage: { + id: '7wxuj38kqm8bz3cmi15vu4w7o', + name: 'Quarter-finals', + urn: '', + }, + multiLeg: { + leg: 1, + relatedMatchId: 's-9ur6e6w5f4ahyxph7ef4rks2c', + }, + period: 'ft', + venue: { + id: '2nrn0y55nz9ee7p9adzbb7fta', + urn: 'urn:bbc:sportsdata:football:venue:s-2nrn0y55nz9ee7p9adzbb7fta', + name: "Stadio Renato Dall'Ara", + shortName: "Stadio Renato Dall'Ara", + }, + attendance: { value: 31142 }, + status: 'PostEvent', + periodLabel: { value: 'FT', accessible: 'Full time' }, + winner: 'away', + tournamentDescriptionLabel: 'UEFA Europa League - Quarter-finals', + groupedActions: [ + { + groupName: { fullName: 'Assists', shortName: 'Assists' }, + homeTeamActions: ["J. Lucumí (90')"], + awayTeamActions: ["Y. Tielemans (44', 90'+4)", "E. Buendía (51')"], + }, + ], + accessibleEventSummary: 'Bologna 1 , Aston Villa 3 at Full time', + sportDiscipline: 'football', + }, + }, +}; diff --git a/src/app/hooks/useSportDataPolling/index.test.ts b/src/app/hooks/useSportDataPolling/index.test.ts new file mode 100644 index 00000000000..19361423451 --- /dev/null +++ b/src/app/hooks/useSportDataPolling/index.test.ts @@ -0,0 +1,111 @@ +import { act, renderHook } from '@testing-library/react'; +import { HeadToHeadV2Data } from '#app/components-webcore/SportDataHeader/head-to-head-v2/types'; +import useSportDataPolling, { POLLING_INTERVAL } from '.'; +import fixtureSportData from './fixture/fixtureSportData'; +import fixtureSportDataUpdate from './fixture/fixtureSportDataUpdate'; +import * as makeRequest from './makeRequest'; + +jest.useFakeTimers(); + +const runPollingInterval = async () => { + await act(async () => { + jest.advanceTimersByTime(POLLING_INTERVAL); + await Promise.resolve(); + }); +}; + +describe('useSportDataPolling', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should return the initial sport data on initialisation', () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + const updatedSportData = + fixtureSportDataUpdate as unknown as HeadToHeadV2Data; + + jest.spyOn(makeRequest, 'default').mockResolvedValue(updatedSportData); + + const { result } = renderHook(() => + useSportDataPolling(initialSportData, true), + ); + + const { currentSportData } = result.current; + + expect(currentSportData).toStrictEqual(initialSportData); + }); + + it('should call makeRequest with the sport data urn when polling is enabled', async () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + const makeRequestSpy = jest + .spyOn(makeRequest, 'default') + .mockResolvedValue(null); + + renderHook(() => useSportDataPolling(initialSportData, true)); + + await runPollingInterval(); + + expect(makeRequestSpy).toHaveBeenCalledTimes(1); + expect(makeRequestSpy).toHaveBeenCalledWith(initialSportData.urn); + }); + + it('should not call makeRequest when polling is disabled', async () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + const makeRequestSpy = jest + .spyOn(makeRequest, 'default') + .mockResolvedValue(null); + + renderHook(() => useSportDataPolling(initialSportData, false)); + + await runPollingInterval(); + + expect(makeRequestSpy).not.toHaveBeenCalled(); + }); + + it('should update current sport data when a poll returns new data', async () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + const updatedSportData = + fixtureSportDataUpdate as unknown as HeadToHeadV2Data; + + jest.spyOn(makeRequest, 'default').mockResolvedValue(updatedSportData); + + const { result } = renderHook(() => + useSportDataPolling(initialSportData, true), + ); + + await runPollingInterval(); + + expect(result.current.currentSportData).toStrictEqual(updatedSportData); + }); + + it('should keep current sport data when poll returns null', async () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + + jest.spyOn(makeRequest, 'default').mockResolvedValue(null); + + const { result } = renderHook(() => + useSportDataPolling(initialSportData, true), + ); + + await runPollingInterval(); + + expect(result.current.currentSportData).toStrictEqual(initialSportData); + }); + + it('should clear the polling interval when unmounted', async () => { + const initialSportData = fixtureSportData as unknown as HeadToHeadV2Data; + const makeRequestSpy = jest + .spyOn(makeRequest, 'default') + .mockResolvedValue(null); + + const { unmount } = renderHook(() => + useSportDataPolling(initialSportData, true), + ); + + unmount(); + + await runPollingInterval(); + + expect(makeRequestSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/app/hooks/useSportDataPolling/index.ts b/src/app/hooks/useSportDataPolling/index.ts new file mode 100644 index 00000000000..66585190a4a --- /dev/null +++ b/src/app/hooks/useSportDataPolling/index.ts @@ -0,0 +1,32 @@ +import { useEffect, useState } from 'react'; +import { HeadToHeadV2Data } from '#app/components-webcore/SportDataHeader/head-to-head-v2/types'; +import makeRequest from './makeRequest'; + +export const POLLING_INTERVAL = 15000; + +const useSportDataPolling = ( + sportData: HeadToHeadV2Data, + enableFeature: boolean, +) => { + const sportDataEventUrn = sportData.urn; + const [currentSportData, setCurrentData] = + useState(sportData); + + useEffect(() => { + const timerId = setInterval(async () => { + if (enableFeature === false) return; + + const polledSportData = await makeRequest(sportDataEventUrn); + + if (polledSportData != null) { + setCurrentData(polledSportData); + } + }, POLLING_INTERVAL); + + return () => clearInterval(timerId); + }, [enableFeature, sportDataEventUrn]); + + return { currentSportData }; +}; + +export default useSportDataPolling; diff --git a/src/app/hooks/useSportDataPolling/makeRequest/index.test.ts b/src/app/hooks/useSportDataPolling/makeRequest/index.test.ts new file mode 100644 index 00000000000..424be944916 --- /dev/null +++ b/src/app/hooks/useSportDataPolling/makeRequest/index.test.ts @@ -0,0 +1,45 @@ +import fixtureSportDataUpdate from '../fixture/fixtureSportDataUpdate'; +import makeRequest from '.'; + +describe('makeRequest', () => { + it('should return data on a valid 200 response where data exists', async () => { + jest.spyOn(global, 'fetch').mockImplementation( + jest.fn(() => + Promise.resolve({ + status: 200, + json: () => Promise.resolve(fixtureSportDataUpdate), + }), + ) as jest.Mock, + ); + + const result = await makeRequest('urn:bbc:sportsdata:football:event:123'); + expect(result).toStrictEqual(fixtureSportDataUpdate.data.sportDataEvent); + }); + + it('should return null on non-200 responses', async () => { + jest.spyOn(global, 'fetch').mockResolvedValue({ + status: 301, + json: async () => fixtureSportDataUpdate, + } as Response); + + const result = await makeRequest('urn:bbc:sportsdata:football:event:123'); + expect(result).toBeNull(); + }); + + it('should return null when fetch throws', async () => { + jest.spyOn(global, 'fetch').mockRejectedValue(new Error('Network error')); + + const result = await makeRequest('urn:bbc:sportsdata:football:event:123'); + expect(result).toBeNull(); + }); + + it('should return null when response data is missing', async () => { + jest.spyOn(global, 'fetch').mockResolvedValue({ + status: 200, + json: async () => ({}), + } as Response); + + const result = await makeRequest('urn:bbc:sportsdata:football:event:123'); + expect(result).toBeNull(); + }); +}); diff --git a/src/app/hooks/useSportDataPolling/makeRequest/index.ts b/src/app/hooks/useSportDataPolling/makeRequest/index.ts new file mode 100644 index 00000000000..334ce94b437 --- /dev/null +++ b/src/app/hooks/useSportDataPolling/makeRequest/index.ts @@ -0,0 +1,25 @@ +import { HeadToHeadV2Data } from '#app/components-webcore/SportDataHeader/head-to-head-v2/types'; +import { getEnvConfig } from '#app/lib/utilities/getEnvConfig'; + +export default async ( + sportDataEventUrn: string, +): Promise => { + try { + const webCdnHost = getEnvConfig().WEB_CDN_URL; + const encodedUrn = encodeURIComponent(sportDataEventUrn); + const fetchUrl = `${webCdnHost}/ws/poll-data/sport?sportDataEventUrn=${encodedUrn}`; + + const response = await fetch(fetchUrl); + const { status } = response; + const { data } = await response.json(); + const sportEventData = data?.sportDataEvent; + + if (status === 200 && sportEventData) { + return sportEventData; + } + + return null; + } catch (_err) { + return null; + } +}; diff --git a/src/app/lib/config/toggles/__snapshots__/index.test.js.snap b/src/app/lib/config/toggles/__snapshots__/index.test.js.snap index 7f8bd7ff5c4..fbcd9b75d2e 100644 --- a/src/app/lib/config/toggles/__snapshots__/index.test.js.snap +++ b/src/app/lib/config/toggles/__snapshots__/index.test.js.snap @@ -79,6 +79,12 @@ exports[`Toggles Config when application environment is live should contain corr "scriptLink": { "enabled": true, }, + "showSportDataHeader": { + "enabled": false, + }, + "sportDataPolling": { + "enabled": false, + }, "topBarOJs": { "enabled": true, }, @@ -174,6 +180,12 @@ exports[`Toggles Config when application environment is local should contain cor "scriptLink": { "enabled": true, }, + "showSportDataHeader": { + "enabled": true, + }, + "sportDataPolling": { + "enabled": true, + }, "topBarOJs": { "enabled": true, }, @@ -269,6 +281,12 @@ exports[`Toggles Config when application environment is test should contain corr "scriptLink": { "enabled": true, }, + "showSportDataHeader": { + "enabled": true, + }, + "sportDataPolling": { + "enabled": true, + }, "topBarOJs": { "enabled": true, }, diff --git a/src/app/lib/config/toggles/liveConfig.js b/src/app/lib/config/toggles/liveConfig.js index d3c6f0975ea..b1ba798bb6a 100644 --- a/src/app/lib/config/toggles/liveConfig.js +++ b/src/app/lib/config/toggles/liveConfig.js @@ -76,6 +76,12 @@ export default { scriptLink: { enabled: true, }, + sportDataPolling: { + enabled: false, + }, + showSportDataHeader: { + enabled: false, + }, topBarOJs: { enabled: true, }, diff --git a/src/app/lib/config/toggles/localConfig.js b/src/app/lib/config/toggles/localConfig.js index 0671f32060c..510fd0b7dbd 100644 --- a/src/app/lib/config/toggles/localConfig.js +++ b/src/app/lib/config/toggles/localConfig.js @@ -77,6 +77,12 @@ export default { scriptLink: { enabled: true, }, + sportDataPolling: { + enabled: true, + }, + showSportDataHeader: { + enabled: true, + }, topBarOJs: { enabled: true, }, diff --git a/src/app/lib/config/toggles/testConfig.js b/src/app/lib/config/toggles/testConfig.js index 3851f1dee99..186e33c65e8 100644 --- a/src/app/lib/config/toggles/testConfig.js +++ b/src/app/lib/config/toggles/testConfig.js @@ -76,6 +76,12 @@ export default { scriptLink: { enabled: true, }, + sportDataPolling: { + enabled: true, + }, + showSportDataHeader: { + enabled: true, + }, topBarOJs: { enabled: true, }, diff --git a/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx b/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx index e86240a6d4e..27e02654264 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/LivePageLayout.tsx @@ -15,7 +15,6 @@ import { HeadToHeadV2Data } from '#app/components-webcore/SportDataHeader/head-t import { PortraitVideoItems } from '#app/models/types/optimo'; import useLivePagePolling from '#app/hooks/useLivePagePolling'; import useToggle from '#app/hooks/useToggle'; -import isLiveEnv from '#app/lib/utilities/isLive'; import { getImageFromPost, getHeadlineFromPost, @@ -88,6 +87,7 @@ const LivePage = ({ pageData, assetId }: LivePageProps) => { } = use(ServiceContext); const { canonicalNonUkLink } = use(RequestContext); const { enabled: livePagePollingEnabled } = useToggle('livePagePolling'); + const { enabled: sportHeaderEnabled } = useToggle('showSportDataHeader'); const streamRef = useRef(null); const [isFirstPostVisible, setIsFirstPostVisible] = useState(true); @@ -112,10 +112,12 @@ const LivePage = ({ pageData, assetId }: LivePageProps) => { const { currentStreamData, hasPendingUpdate, applyPendingUpdate } = useLivePagePolling(pageData, livePagePollingEnabled && isLive); - const sportData = sportDataEventContent?.sportDataEvent; - const isSportDataLive = sportDataEventContent?.live; - const sportDataTitle = sportDataEventContent?.title; - const showSportData = !!sportData && !isLiveEnv(); + const { + sportDataEvent: sportData, + live: isSportDataLive, + title: sportDataTitle, + } = sportDataEventContent || {}; + const showSportData = !!sportData && Boolean(sportHeaderEnabled); const { url: imageUrl, @@ -225,9 +227,10 @@ const LivePage = ({ pageData, assetId }: LivePageProps) => { /> {showSportData && ( )}
diff --git a/ws-nextjs-app/pages/[service]/live/[id]/live.stories.tsx b/ws-nextjs-app/pages/[service]/live/[id]/live.stories.tsx index 653aa5d2899..860104300aa 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/live.stories.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/live.stories.tsx @@ -33,6 +33,11 @@ const Component = ({ pageData }: ComponentProps) => ( export default { title: 'Pages/Live Page', Component, + globals: { + toggles: { + showSportDataHeader: { enabled: true }, + }, + }, parameters: { layout: 'fullscreen' }, }; diff --git a/ws-nextjs-app/pages/[service]/live/[id]/live.test.tsx b/ws-nextjs-app/pages/[service]/live/[id]/live.test.tsx index 15cdc5ced23..794e94a261f 100644 --- a/ws-nextjs-app/pages/[service]/live/[id]/live.test.tsx +++ b/ws-nextjs-app/pages/[service]/live/[id]/live.test.tsx @@ -11,7 +11,7 @@ import sportDataFixture from '#data/afrique/live/c7gk1vjglxn1t.json'; import { GetServerSidePropsContext } from 'next'; import MockIntersectionObserver from '#app/components/intersection-observer-testing-library'; import * as useLivePagePolling from '#app/hooks/useLivePagePolling'; -import * as isLiveEnvModule from '#app/lib/utilities/isLive'; +import useToggle from '#app/hooks/useToggle'; import Live, { ComponentProps } from './LivePageLayout'; import { getServerSideProps } from './[[...variant]].page'; import { StreamResponse } from './Post/types'; @@ -29,14 +29,19 @@ jest.mock('#app/lib/utilities/isLive', () => ({ jest.mock('#app/components-webcore/SportDataHeader/head-to-head-v2', () => ({ __esModule: true, default: jest.fn( - ({ data, isConciseView, shouldHideBadges, shouldShowActions }) => ( + ({ + initialSportData, + isConciseView, + shouldHideBadges, + shouldShowActions, + }) => (
- {data?.home?.fullName} vs {data?.away?.fullName} + {initialSportData?.home?.fullName} vs {initialSportData?.away?.fullName}
), ), @@ -47,6 +52,11 @@ jest.mock('#app/components/PortraitVideoCarousel', () => ({ default: jest.fn(() =>
), })); +jest.mock('#app/hooks/useToggle', () => ({ + __esModule: true, + default: jest.fn(() => ({ enabled: true })), +})); + type HelmetMetaTag = { property?: string; content?: string; @@ -59,6 +69,7 @@ const mockPageData = { block: 'Its a block', }, liveTextStream: { + id: 'mock-stream-id', content: { data: { results: [], @@ -88,6 +99,7 @@ const mockPageDataWithPosts = { block: 'Its a block', }, liveTextStream: { + id: 'mock-stream-id', content: postFixture, contributors: 'Not a random dude', }, @@ -104,6 +116,7 @@ const mockPageDataWithoutKeyPoints = { content: null, }, liveTextStream: { + id: 'mock-stream-id', content: postFixture, contributors: 'Not a random dude', }, @@ -116,9 +129,9 @@ const mockPageDataWithPortraitVideoItems = { portraitVideo: { blocks: [ { - type: 'portraitClipMedia', + type: 'portraitClipMedia' as const, model: { - type: 'video', + type: 'video' as const, images: [ { source: @@ -411,6 +424,7 @@ describe('Live Page', () => { const paginatedData = { ...mockPageData, liveTextStream: { + id: 'mock-stream-id', content: { data: { results: [], @@ -446,6 +460,7 @@ describe('Live Page', () => { dateModified: '2024-03-12T11:00:52+00:00', }, liveTextStream: { + id: 'mock-stream-id', content: { data: { results: [], @@ -668,6 +683,7 @@ describe('Live Page', () => { live: true, }, } as unknown as ComponentProps['pageData']; + mockPollingUpdate(pageDataWithSportData); await act(async () => { @@ -803,14 +819,14 @@ describe('Live Page', () => { expect(screen.queryByTestId('head-to-head-v2')).not.toBeInTheDocument(); }); - it('should not render HeadToHeadV2 when in live environment', async () => { + it('should not render HeadToHeadV2 when sportHeaderEnabled toggle is disabled', async () => { const pageDataWithSportData = { ...mockPageData, sportDataEventContent: sportDataFixture.data.sportDataEventContent, } as unknown as ComponentProps['pageData']; mockPollingUpdate(pageDataWithSportData); - jest.spyOn(isLiveEnvModule, 'default').mockReturnValue(true); + (useToggle as jest.Mock).mockReturnValue({ enabled: false }); await act(async () => { render(); @@ -818,7 +834,7 @@ describe('Live Page', () => { expect(screen.queryByTestId('head-to-head-v2')).not.toBeInTheDocument(); - jest.spyOn(isLiveEnvModule, 'default').mockReturnValue(false); + (useToggle as jest.Mock).mockReturnValue({ enabled: true }); }); it('should render Header when sportDataEventContent is not present', async () => {