Жабдыгыңыз медианын бул түрүн ойнотууга ылайыктуу эмес.
@@ -155,7 +155,7 @@ exports[`Canonical Kyrgyz Live Radio Page Media Loader renders a valid container
exports[`Canonical Kyrgyz Live Radio Page SEO Apple Touch Icon should match attributes 1`] = `
{
"sizes": null,
- "url": "http://localhost:7080/kyrgyz/images/icons/icon-192x192.png",
+ "url": "http://localhost:7081/kyrgyz/images/icons/icon-192x192.png",
}
`;
@@ -187,7 +187,7 @@ exports[`Canonical Kyrgyz Live Radio Page SEO Apple Touch Icon should match attr
}
`;
-exports[`Canonical Kyrgyz Live Radio Page SEO Canonical link 1`] = `"http://localhost:7080/kyrgyz/bbc_kyrgyz_radio/liveradio"`;
+exports[`Canonical Kyrgyz Live Radio Page SEO Canonical link 1`] = `"http://localhost:7081/kyrgyz/bbc_kyrgyz_radio/liveradio"`;
exports[`Canonical Kyrgyz Live Radio Page SEO Dir attribute 1`] = `"ltr"`;
@@ -215,11 +215,11 @@ exports[`Canonical Kyrgyz Live Radio Page SEO Linked data should match text 1`]
"name": "Kyrgyz",
},
"mainEntityOfPage": {
- "@id": "http://localhost:7080/kyrgyz/bbc_kyrgyz_radio/liveradio",
+ "@id": "http://localhost:7081/kyrgyz/bbc_kyrgyz_radio/liveradio",
"@type": "WebPage",
"name": "Би-Би-Си Кыргыз кызматынын радиосу",
},
- "url": "http://localhost:7080/kyrgyz/bbc_kyrgyz_radio/liveradio",
+ "url": "http://localhost:7081/kyrgyz/bbc_kyrgyz_radio/liveradio",
},
],
}
@@ -239,7 +239,7 @@ exports[`Canonical Kyrgyz Live Radio Page SEO OG title 1`] = `"Би-Би-Си К
exports[`Canonical Kyrgyz Live Radio Page SEO OG type 1`] = `"website"`;
-exports[`Canonical Kyrgyz Live Radio Page SEO OG url 1`] = `"http://localhost:7080/kyrgyz/bbc_kyrgyz_radio/liveradio"`;
+exports[`Canonical Kyrgyz Live Radio Page SEO OG url 1`] = `"http://localhost:7081/kyrgyz/bbc_kyrgyz_radio/liveradio"`;
exports[`Canonical Kyrgyz Live Radio Page SEO Page title 1`] = `"Би-Би-Си Кыргыз кызматынын радиосу - BBC News Кыргыз Кызматы"`;
diff --git a/src/integration/pages/liveRadio/kyrgyz/canonical.test.js b/ws-nextjs-app/integration/pages/liveRadio/kyrgyz/canonical.test.ts
similarity index 100%
rename from src/integration/pages/liveRadio/kyrgyz/canonical.test.js
rename to ws-nextjs-app/integration/pages/liveRadio/kyrgyz/canonical.test.ts
diff --git a/ws-nextjs-app/package.json b/ws-nextjs-app/package.json
index fd688e37546..cd57f8e9e6a 100644
--- a/ws-nextjs-app/package.json
+++ b/ws-nextjs-app/package.json
@@ -24,6 +24,8 @@
"test:integration:amp": "NODE_OPTIONS=--no-experimental-strip-types jest --ci --colors --selectProjects='Integration Tests - AMP'",
"test:integration:lite": "NODE_OPTIONS=--no-experimental-strip-types jest --ci --colors --selectProjects='Integration Tests - Lite'",
"cypress": "cypress run --browser chrome",
+ "cypress:onefile": "CYPRESS_APP_ENV=live CYPRESS_SMOKE=false cypress run --spec './cypress/e2e/articlePage/*'",
+ "cypress:oneserviceandonefile": "CYPRESS_APP_ENV=live CYPRESS_SMOKE=false CYPRESS_ONLY_SERVICE=mundo cypress run --spec './cypress/e2e/articlePage/*'",
"cypress:interactive": "cypress open --browser chrome",
"test:e2e": "yarn stop && yarn build && run-p --race start cypress -- --e2e",
"test:e2e:interactive": "yarn stop && yarn build && run-p --race start cypress:interactive -- --e2e"
diff --git a/ws-nextjs-app/pages/[service]/[[...]].page.tsx b/ws-nextjs-app/pages/[service]/[[...]].page.tsx
index f59111e1ad7..78f6e2d8fcf 100644
--- a/ws-nextjs-app/pages/[service]/[[...]].page.tsx
+++ b/ws-nextjs-app/pages/[service]/[[...]].page.tsx
@@ -14,6 +14,7 @@ import {
HOME_PAGE,
AUDIO_PAGE,
TV_PAGE,
+ LIVE_RADIO_PAGE,
} from '#app/routes/utils/pageTypes';
import { PageTypes } from '#app/models/types/global';
import PageDataParams from '#app/models/types/pageDataParams';
@@ -22,6 +23,7 @@ import withOptimizelyProvider from '#app/legacy/containers/PageHandlers/withOpti
import { HomePageProps } from '#app/pages/HomePage/HomePage';
import { getEnvConfig } from '#app/lib/utilities/getEnvConfig';
import derivePageType from '#nextjs/utilities/derivePageType';
+import { LiveRadioPageProps } from '#app/pages/LiveRadioPage/types';
// AV Embeds
import withMediaError from '#app/lib/utilities/episodeAvailability/withMediaError';
@@ -39,6 +41,8 @@ import handleOnDemandAudioRoute from './onDemandAudio/handleOnDemandAudioRoute';
import { OnDemandAudioProps } from './onDemandAudio/types';
// On Demand TV
import handleOnDemandTvRoute from './onDemandTv/handleOnDemandTvRoute';
+// Live Radio
+import handleLiveRadioRoute from './liveRadio/handleLiveRadioRoute';
// Dynamic imports of page layouts
const AvEmbedsPageLayout = dynamic(
@@ -56,6 +60,10 @@ const OnDemandTvPage = dynamic(
() => import('#app/pages/OnDemandTvPage/OnDemandTvPage'),
);
+const LiveRadioPage = dynamic(
+ () => import('#app/pages/LiveRadioPage/LiveRadioPage'),
+);
+
const getPageType = ({
resolvedUrl,
reqHeaders,
@@ -87,6 +95,7 @@ const ROUTE_HANDLERS = {
[HOME_PAGE]: handleHomepageRoute,
[AUDIO_PAGE]: handleOnDemandAudioRoute,
[TV_PAGE]: handleOnDemandTvRoute,
+ [LIVE_RADIO_PAGE]: handleLiveRadioRoute,
};
export const getServerSideProps: GetServerSideProps = async context => {
@@ -129,7 +138,8 @@ type PageProps = {
ArticlePageProps &
HomePageProps &
OnDemandAudioProps &
- OnDemandTVProps;
+ OnDemandTVProps &
+ LiveRadioPageProps;
export default function PageTypeToRender({ pageType, ...props }: PageProps) {
switch (pageType) {
@@ -149,6 +159,8 @@ export default function PageTypeToRender({ pageType, ...props }: PageProps) {
return withMediaError(OnDemandAudioPage)({ ...props });
case TV_PAGE:
return withMediaError(OnDemandTvPage)({ ...props });
+ case LIVE_RADIO_PAGE:
+ return withMediaError(LiveRadioPage)({ ...props });
// Home Page
case HOME_PAGE:
return withOptimizelyProvider(HomePage)({ ...props });
diff --git a/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.test.ts b/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.test.ts
new file mode 100644
index 00000000000..44c5a7504fb
--- /dev/null
+++ b/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.test.ts
@@ -0,0 +1,174 @@
+import { GetServerSidePropsContext } from 'next';
+import liveRadioJson from '#data/korean/bbc_korean_radio/liveradio.json';
+import { LIVE_RADIO_PAGE } from '#app/routes/utils/pageTypes';
+import { Toggles } from '#app/models/types/global';
+import * as getTogglesModule from '#app/lib/utilities/getToggles/withCache';
+import * as getPageDataModule from '../../../utilities/pageRequests/getPageData';
+import handleLiveRadioRoute from './handleLiveRadioRoute';
+
+jest.mock('../../../utilities/pageRequests/getPageData');
+jest.mock('#app/lib/utilities/getToggles/withCache');
+
+describe('handleLiveRadioRoute', () => {
+ const mockSetHeader = jest.fn();
+ const mockGetServerSidePropsContext = {
+ req: {
+ headers: {},
+ } as unknown as GetServerSidePropsContext['req'],
+ res: {
+ setHeader: mockSetHeader,
+ removeHeader: jest.fn(),
+ } as unknown as GetServerSidePropsContext['res'],
+ resolvedUrl: '/korean/bbc_korean_radio/liveradio',
+ query: { service: 'korean' },
+ } satisfies GetServerSidePropsContext;
+
+ let getPageDataSpy: jest.SpyInstance;
+
+ beforeEach(() => {
+ jest.restoreAllMocks();
+ jest.clearAllMocks();
+ getPageDataSpy = jest
+ .spyOn(getPageDataModule, 'default')
+ .mockResolvedValue({
+ data: {
+ pageData: liveRadioJson.data,
+ status: 200,
+ },
+ });
+
+ jest.spyOn(getTogglesModule, 'default').mockResolvedValue({
+ liveRadioSchedule: { enabled: true },
+ } as Toggles);
+ });
+
+ it('returns expected props if data fetch succeeds', async () => {
+ const { props } = await handleLiveRadioRoute(mockGetServerSidePropsContext);
+ expect(props.status).toEqual(200);
+ expect(props.pageType).toEqual('liveRadio');
+ expect(props.pageData).toEqual(liveRadioJson.data);
+ });
+
+ it('should return essential data for a page to render', async () => {
+ const {
+ props: { pageData },
+ } = await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(pageData.name).toEqual('BBC 코리아 라디오');
+ expect(pageData.language).toEqual('ko');
+ expect(pageData.metadata.type).toEqual('Live Radio');
+ expect(pageData.summary).toEqual(
+ '세계와 한반도 뉴스를 공정하고 객관적으로 전달해 드립니다',
+ );
+ expect(pageData.heading).toEqual('BBC 코리아 라디오');
+ expect(pageData.bodySummary).toEqual(
+ '세계와 한반도 뉴스를 공정하고 객관적으로 전달해 드립니다',
+ );
+ expect(pageData.masterBrand).toEqual('bbc_korean_radio');
+ });
+
+ it('should call getPageData with the correct arguments', async () => {
+ await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(getPageDataSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: '/korean/bbc_korean_radio/liveradio',
+ service: 'korean',
+ pageType: LIVE_RADIO_PAGE,
+ resolvedUrl: '/korean/bbc_korean_radio/liveradio',
+ }),
+ );
+ });
+
+ it('should pass disableRadioSchedule as true when toggle is disabled', async () => {
+ jest.spyOn(getTogglesModule, 'default').mockResolvedValue({
+ liveRadioSchedule: { enabled: false },
+ } as Toggles);
+
+ await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(getPageDataSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ disableRadioSchedule: true,
+ }),
+ );
+ });
+
+ it('should pass disableRadioSchedule as false when toggle is enabled', async () => {
+ await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(getPageDataSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ disableRadioSchedule: false,
+ }),
+ );
+ });
+
+ it('returns error props if data fetch returns 500', async () => {
+ jest.spyOn(getPageDataModule, 'default').mockResolvedValue({
+ data: {
+ pageData: liveRadioJson.data,
+ status: 500,
+ },
+ });
+
+ const result = await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(result).toEqual({
+ props: expect.objectContaining({
+ status: 500,
+ pageType: 'liveRadio',
+ pathname: '/korean/bbc_korean_radio/liveradio',
+ }),
+ });
+ });
+
+ it('returns error props if data fetch returns 404', async () => {
+ jest.spyOn(getPageDataModule, 'default').mockResolvedValue({
+ data: {
+ pageData: liveRadioJson.data,
+ status: 404,
+ },
+ });
+
+ const result = await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(result).toEqual({
+ props: expect.objectContaining({
+ status: 404,
+ pageType: 'liveRadio',
+ pathname: '/korean/bbc_korean_radio/liveradio',
+ }),
+ });
+ });
+
+ it('throws if pageData is missing', async () => {
+ jest.spyOn(getPageDataModule, 'default').mockResolvedValue({
+ data: { pageData: null, status: 200 },
+ });
+
+ await expect(
+ handleLiveRadioRoute(mockGetServerSidePropsContext),
+ ).rejects.toThrow('LiveRadioPage data is malformed');
+ });
+
+ it('sets correct cache-control header', async () => {
+ await handleLiveRadioRoute(mockGetServerSidePropsContext);
+
+ expect(mockSetHeader).toHaveBeenCalledWith(
+ 'Cache-Control',
+ expect.stringContaining('max-age=30'),
+ );
+ });
+
+ it('returns not found props when service is invalid', async () => {
+ const result = await handleLiveRadioRoute({
+ ...mockGetServerSidePropsContext,
+ resolvedUrl: '/fakeservice',
+ });
+
+ expect(result).toEqual({
+ props: expect.objectContaining({ status: 404 }),
+ });
+ });
+});
diff --git a/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.ts b/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.ts
new file mode 100644
index 00000000000..81731e6f1d3
--- /dev/null
+++ b/ws-nextjs-app/pages/[service]/liveRadio/handleLiveRadioRoute.ts
@@ -0,0 +1,104 @@
+import { LIVE_RADIO_PAGE } from '#app/routes/utils/pageTypes';
+import { ROUTING_INFORMATION } from '#lib/logger.const';
+import nodeLogger from '#lib/logger.node';
+import { GetServerSidePropsContext } from 'next';
+import PageDataParams from '#app/models/types/pageDataParams';
+import parseRoute from '#app/routes/utils/parseRoute';
+import { NOT_FOUND, OK } from '#app/lib/statusCodes.const';
+import getPageData from '#nextjs/utilities/pageRequests/getPageData';
+import handleError from '#app/routes/utils/handleError';
+import getToggles from '#app/lib/utilities/getToggles/withCache';
+
+const logger = nodeLogger(__filename);
+
+export default async (context: GetServerSidePropsContext) => {
+ const { resolvedUrl } = context;
+ const { renderer_env: rendererEnv } = context.query as PageDataParams;
+
+ const resolvedUrlWithoutQuery = resolvedUrl.split('?')?.[0];
+
+ const { service, variant } = parseRoute(resolvedUrl);
+
+ const toggles = await getToggles(service);
+ const { enabled: scheduleIsEnabled } = toggles.liveRadioSchedule;
+ const disableRadioSchedule = !scheduleIsEnabled;
+
+ if (!service) {
+ context.res.statusCode = NOT_FOUND;
+
+ return {
+ props: {
+ service,
+ status: NOT_FOUND,
+ timeOnServer: Date.now(),
+ variant: variant || null,
+ pageType: LIVE_RADIO_PAGE,
+ pathname: resolvedUrlWithoutQuery,
+ },
+ };
+ }
+
+ const { data } = await getPageData({
+ id: resolvedUrlWithoutQuery,
+ service,
+ variant: variant || undefined,
+ rendererEnv,
+ resolvedUrl: resolvedUrlWithoutQuery,
+ pageType: LIVE_RADIO_PAGE,
+ disableRadioSchedule,
+ });
+
+ const { pageData, status } = data;
+
+ context.res.statusCode = status;
+
+ let routingInfoLogger = logger.debug;
+
+ if (status !== OK) {
+ routingInfoLogger = logger.error;
+
+ routingInfoLogger(ROUTING_INFORMATION, {
+ url: resolvedUrlWithoutQuery,
+ status,
+ pageType: LIVE_RADIO_PAGE,
+ });
+
+ return {
+ props: {
+ service,
+ status,
+ timeOnServer: Date.now(),
+ variant: variant || null,
+ pageType: LIVE_RADIO_PAGE,
+ pathname: resolvedUrlWithoutQuery,
+ },
+ };
+ }
+
+ if (!pageData) {
+ throw handleError('LiveRadioPage data is malformed', 500);
+ }
+
+ context.res.setHeader(
+ 'Cache-Control',
+ 'public, stale-if-error=300, stale-while-revalidate=120, max-age=30',
+ );
+
+ routingInfoLogger(ROUTING_INFORMATION, {
+ url: resolvedUrlWithoutQuery,
+ status,
+ pageType: LIVE_RADIO_PAGE,
+ });
+
+ return {
+ props: {
+ id: resolvedUrlWithoutQuery,
+ pageData,
+ pageType: LIVE_RADIO_PAGE,
+ pathname: resolvedUrlWithoutQuery,
+ service,
+ status,
+ variant: variant || null,
+ },
+ };
+};
diff --git a/ws-nextjs-app/utilities/derivePageType/index.test.ts b/ws-nextjs-app/utilities/derivePageType/index.test.ts
index 97467ab3bc7..4391a886402 100644
--- a/ws-nextjs-app/utilities/derivePageType/index.test.ts
+++ b/ws-nextjs-app/utilities/derivePageType/index.test.ts
@@ -10,6 +10,7 @@ import {
AUDIO_PAGE,
TV_PAGE,
MOST_READ_PAGE,
+ LIVE_RADIO_PAGE,
} from '#app/routes/utils/pageTypes';
import derivePageType from '.';
@@ -116,6 +117,12 @@ describe('derivePageType', () => {
expect(result).toEqual(MOST_READ_PAGE);
});
+ it('should return LIVE_RADIO_PAGE if pathname includes live-radio', () => {
+ const pathname = '/hausa/bbc_hausa_radio/liveradio';
+ const result = derivePageType(pathname);
+ expect(result).toEqual(LIVE_RADIO_PAGE);
+ });
+
it('should return Unknown if pathname does not include live or send', () => {
const pathname = '/pidgin/xxxxxxxxx';
const result = derivePageType(pathname);
diff --git a/ws-nextjs-app/utilities/derivePageType/index.ts b/ws-nextjs-app/utilities/derivePageType/index.ts
index 908349384de..e02edcc8ffa 100644
--- a/ws-nextjs-app/utilities/derivePageType/index.ts
+++ b/ws-nextjs-app/utilities/derivePageType/index.ts
@@ -11,6 +11,7 @@ import {
AUDIO_PAGE,
TV_PAGE,
MOST_READ_PAGE,
+ LIVE_RADIO_PAGE,
} from '#app/routes/utils/pageTypes';
import {
isOptimoIdCheck,
@@ -47,6 +48,9 @@ const isOnDemandAudioPodcastPath = (pathname: string) =>
const isOnDemandTvPath = (pathname: string) =>
/\/bbc_[a-z]+_tv\/(?:tv|tv_programmes)\//.test(pathname);
+const isLiveRadioPath = (pathname: string) =>
+ /\/bbc_[a-z]+_radio\/liveradio/.test(pathname);
+
export default function derivePageType(pathname: string): PageTypes {
const sanitisedPathname = new URL(
removeRendererExtension(pathname),
@@ -58,6 +62,7 @@ export default function derivePageType(pathname: string): PageTypes {
if (sanitisedPathname.includes('popular/read')) return MOST_READ_PAGE;
if (isUgcIdCheck(sanitisedPathname)) return UGC_PAGE;
+ if (isLiveRadioPath(sanitisedPathname)) return LIVE_RADIO_PAGE;
if (isTipoIdCheck(sanitisedPathname) && sanitisedPathname.includes('topics'))
return TOPIC_PAGE;
if (isTipoIdCheck(sanitisedPathname) && sanitisedPathname.includes('live'))
diff --git a/ws-nextjs-app/utilities/pageRequests/getPageData.ts b/ws-nextjs-app/utilities/pageRequests/getPageData.ts
index 7959b4c69c6..4a1940275e2 100644
--- a/ws-nextjs-app/utilities/pageRequests/getPageData.ts
+++ b/ws-nextjs-app/utilities/pageRequests/getPageData.ts
@@ -18,6 +18,7 @@ type Props = {
resolvedUrl: string;
pageType: PageTypes;
isAmp?: boolean;
+ disableRadioSchedule?: boolean;
};
const getPageData = async ({
@@ -29,6 +30,7 @@ const getPageData = async ({
resolvedUrl,
pageType,
isAmp,
+ disableRadioSchedule,
}: Props) => {
const path = `${id}${rendererEnv ? `?renderer_env=${rendererEnv}` : ''}`;
const url = new URL(path, 'https://www.bbc.com');
@@ -48,6 +50,7 @@ const getPageData = async ({
page,
getAgent,
isAmp,
+ disableRadioSchedule,
}));
} catch (error: unknown) {
({ message, status } = error as FetchError);