diff --git a/data/mundo/articles/cle16n19nd9o.json b/data/mundo/articles/cle16n19nd9o.json
new file mode 100644
index 00000000000..02edb1f6c0e
--- /dev/null
+++ b/data/mundo/articles/cle16n19nd9o.json
@@ -0,0 +1,1015 @@
+{
+ "data": {
+ "article": {
+ "metadata": {
+ "atiAnalytics": {
+ "categoryName": null,
+ "contentId": "urn:bbc:optimo:asset:cle16n19nd9o",
+ "contentType": "article",
+ "language": "es",
+ "ldpThingIds": null,
+ "ldpThingLabels": null,
+ "nationsProducer": null,
+ "pageIdentifier": "mundo.articles.cle16n19nd9o.page",
+ "pageTitle": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "timePublished": "2023-12-13T17:26:44.332Z",
+ "timeUpdated": "2023-12-13T17:26:44.332Z"
+ },
+ "id": "urn:bbc:ares::article:cle16n19nd9o",
+ "locators": {
+ "optimoUrn": "urn:bbc:optimo:asset:cle16n19nd9o",
+ "canonicalUrl": "https://www.bbc.com/mundo/articles/cle16n19nd9o"
+ },
+ "type": "article",
+ "createdBy": "Mundo",
+ "language": "es",
+ "firstPublished": 1702488404332,
+ "lastPublished": 1702488404332,
+ "options": { "includeComments": false },
+ "analyticsLabels": {
+ "contentId": "urn:bbc:optimo:asset:cle16n19nd9o",
+ "producer": "Mundo",
+ "page": "mundo.articles.cle16n19nd9o.page",
+ "irisKeyword": null
+ },
+ "passport": {
+ "language": "es",
+ "home": "http://www.bbc.co.uk/ontologies/passport/home/Mundo",
+ "taggings": [
+ {
+ "predicate": "http://www.bbc.co.uk/ontologies/bbc/infoClass",
+ "value": "http://www.bbc.co.uk/things/0db2b959-cbf8-4661-965f-050974a69bb5#id"
+ },
+ {
+ "predicate": "http://www.bbc.co.uk/ontologies/bbc/assetType",
+ "value": "http://www.bbc.co.uk/things/22ea958e-2004-4f34-80a7-bf5acad52f6f#id"
+ }
+ ],
+ "predicates": {
+ "infoClass": [
+ {
+ "value": "http://www.bbc.co.uk/things/0db2b959-cbf8-4661-965f-050974a69bb5#id",
+ "type": "infoClass"
+ }
+ ],
+ "assetType": [
+ {
+ "value": "http://www.bbc.co.uk/things/22ea958e-2004-4f34-80a7-bf5acad52f6f#id",
+ "type": "assetType"
+ }
+ ]
+ }
+ },
+ "blockTypes": [
+ "headline",
+ "text",
+ "paragraph",
+ "fragment",
+ "video",
+ "caption",
+ "aresMedia",
+ "aresMediaMetadata",
+ "image",
+ "rawImage",
+ "altText"
+ ],
+ "includeComments": false,
+ "consumableAsSFV": false,
+ "allowAdvertising": true,
+ "consumableOnRedButton": false,
+ "consumableOnlyOnRedButton": false,
+ "breakingNews": { "isBreaking": false },
+ "useSensitiveOnwardJourneys": false,
+ "stats": { "readTime": 1, "wordCount": 218 },
+ "isTransliterated": false
+ },
+ "content": {
+ "model": {
+ "blocks": [
+ {
+ "id": "0ae92caf",
+ "type": "headline",
+ "model": {
+ "blocks": [
+ {
+ "id": "ad98840d",
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "id": "9bfe3a72",
+ "type": "paragraph",
+ "model": {
+ "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "blocks": [
+ {
+ "id": "a5eb772d",
+ "type": "fragment",
+ "model": {
+ "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "attributes": []
+ },
+ "position": [1, 1, 1, 1]
+ }
+ ]
+ },
+ "position": [1, 1, 1]
+ }
+ ]
+ },
+ "position": [1, 1]
+ }
+ ]
+ },
+ "position": [1]
+ },
+ {
+ "id": "6cefd73a",
+ "type": "timestamp",
+ "model": {
+ "firstPublished": 1702488404332,
+ "lastPublished": 1702488404332
+ },
+ "position": [2]
+ },
+ {
+ "id": "50eceb9a",
+ "type": "text",
+ "model": {
+ "suitableForAbridgement": false,
+ "blocks": [
+ {
+ "id": "1d48acf6",
+ "type": "paragraph",
+ "model": {
+ "text": "Proporcionar a las autoridades de producción llegar rápidamente a cualquier forma influyente de software India agrega oportunidades globales en un entorno favorable al dar las consideraciones iniciales de Forex para crear oficinas, su mensaje de estado",
+ "blocks": [
+ {
+ "id": "65ffdb12",
+ "type": "fragment",
+ "model": {
+ "text": "Proporcionar a las autoridades de producción llegar rápidamente a cualquier forma influyente de software India agrega oportunidades globales en un entorno favorable al dar las consideraciones iniciales de Forex para crear oficinas, su mensaje de estado",
+ "attributes": []
+ },
+ "position": [3, 1, 1]
+ }
+ ]
+ },
+ "position": [3, 1]
+ },
+ {
+ "id": "9c7e2a1d",
+ "type": "paragraph",
+ "model": {
+ "text": "La información amigable para la vida, su punto de vista, discusión, estructura, mercado, etc., los objetivos principales de focalización son los mismos. El compromiso de orientación de compra está informado. En este momento, el tema de la libertad en inglés no está realmente narrado por la sección. Jaane Dishame World Hardware Necesario Grupo de trabajo de consulta de brujería pero",
+ "blocks": [
+ {
+ "id": "2650cd66",
+ "type": "fragment",
+ "model": {
+ "text": "La información amigable para la vida, su punto de vista, discusión, estructura, mercado, etc., los objetivos principales de focalización son los mismos. El compromiso de orientación de compra está informado. En este momento, el tema de la libertad en inglés no está realmente narrado por la sección. Jaane Dishame World Hardware Necesario Grupo de trabajo de consulta de brujería pero",
+ "attributes": []
+ },
+ "position": [3, 2, 1]
+ }
+ ]
+ },
+ "position": [3, 2]
+ }
+ ]
+ },
+ "position": [3]
+ },
+ {
+ "id": "033f937b",
+ "type": "video",
+ "model": {
+ "locator": "urn:bbc:pips:pid:p01vvrqv",
+ "blocks": [
+ {
+ "id": "6273acc2",
+ "type": "caption",
+ "model": {
+ "blocks": [
+ {
+ "id": "812665b4",
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "id": "02ff1171",
+ "type": "paragraph",
+ "model": {
+ "text": "Título de vídeo",
+ "blocks": [
+ {
+ "id": "4ebdfe79",
+ "type": "fragment",
+ "model": {
+ "text": "Título de vídeo",
+ "attributes": []
+ },
+ "position": [4, 1, 1, 1, 1]
+ }
+ ]
+ },
+ "position": [4, 1, 1, 1]
+ }
+ ]
+ },
+ "position": [4, 1, 1]
+ }
+ ]
+ },
+ "position": [4, 1]
+ },
+ {
+ "id": "7c9042a8",
+ "type": "aresMedia",
+ "model": {
+ "blocks": [
+ {
+ "id": "13107ff5",
+ "blockId": "urn:bbc:ares::clip:p01vvrqv",
+ "type": "aresMediaMetadata",
+ "model": {
+ "id": "p01vvrqv",
+ "subType": "clip",
+ "format": "video",
+ "title": "Mundo test video for transcription",
+ "synopses": {
+ "short": "Mundo test video for transcription"
+ },
+ "imageUrl": "ichef.test.bbci.co.uk/images/ic/$recipe/p01vvs5g.jpg",
+ "embedding": true,
+ "advertising": true,
+ "versions": [
+ {
+ "versionId": "p01vvrqx",
+ "types": ["Original"],
+ "duration": 59,
+ "durationISO8601": "PT59S",
+ "warnings": {},
+ "availableTerritories": {
+ "uk": true,
+ "nonUk": true
+ },
+ "availableFrom": 1702549938000
+ }
+ ],
+ "syndication": { "destinations": [] },
+ "smpKind": "programme",
+ "webcastVersions": []
+ },
+ "position": [4, 2, 1]
+ },
+ {
+ "id": "0b8df686",
+ "type": "image",
+ "model": {
+ "blocks": [
+ {
+ "id": "1538f965",
+ "type": "rawImage",
+ "model": {
+ "width": 640,
+ "height": 360,
+ "locator": "ichef.test.bbci.co.uk/images/ic/$widthxn/p01vvs5g.jpg",
+ "originCode": "mpv"
+ },
+ "position": [4, 2, 2, 1]
+ },
+ {
+ "id": "a54d5b2e",
+ "type": "altText",
+ "model": {
+ "blocks": [
+ {
+ "id": "37bab97f",
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "id": "a145f773",
+ "type": "paragraph",
+ "model": {
+ "text": "Keyframe #1",
+ "blocks": [
+ {
+ "id": "d5eecf9b",
+ "type": "fragment",
+ "model": {
+ "text": "Keyframe #1",
+ "attributes": []
+ },
+ "position": [
+ 4, 2, 2, 2, 1, 1, 1
+ ]
+ }
+ ]
+ },
+ "position": [4, 2, 2, 2, 1, 1]
+ }
+ ]
+ },
+ "position": [4, 2, 2, 2, 1]
+ }
+ ]
+ },
+ "position": [4, 2, 2, 2]
+ }
+ ]
+ },
+ "position": [4, 2, 2]
+ }
+ ]
+ },
+ "position": [4, 2]
+ },
+ {
+ "id": "cf1f2a7a",
+ "type": "transcript",
+ "model": {
+ "language": "es-ES",
+ "blocks": [
+ {
+ "id": "adf9d024",
+ "start": "00:00:00.060",
+ "content": "Na nutsu."
+ },
+ {
+ "id": "39141eb1",
+ "start": "00:00:01.020",
+ "content": "Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina."
+ },
+ {
+ "id": "dd490f81",
+ "start": "00:00:04.770",
+ "content": "Na 1938 Austin goma Cambridge."
+ },
+ {
+ "id": "a24662a1",
+ "start": "00:00:14.630",
+ "content": "A koyaushe ina sha'awar tarihin cewa akwaia cikin daji."
+ },
+ {
+ "id": "59b44993",
+ "start": "00:00:18.020",
+ "content": "Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon."
+ },
+ {
+ "id": "597e6271",
+ "start": "00:00:21.770",
+ "content": "Ba zai taba zama motar zamani ba."
+ },
+ {
+ "id": "f6d43c09",
+ "start": "00:00:24.680",
+ "content": "1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara"
+ },
+ {
+ "id": "4afed1db",
+ "start": "00:00:28.448",
+ "content": "tarawa ɗaya."
+ },
+ {
+ "id": "a0a31ae3",
+ "start": "00:00:29.060",
+ "content": "Don haka akwai 'yan kudin aljihu daabubuwa makamantansu."
+ },
+ {
+ "id": "2b5b928a",
+ "start": "00:00:31.610",
+ "content": "Ƙananan ayyuka."
+ },
+ {
+ "id": "cf6156ec",
+ "start": "00:00:32.330",
+ "content": "Zan sake yin ɗan kuɗin aljihu, ajiye shi."
+ },
+ {
+ "id": "7251175f",
+ "start": "00:00:35.270",
+ "content": "Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na,"
+ },
+ {
+ "id": "372abf83",
+ "start": "00:00:39.178",
+ "content": "sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri."
+ },
+ {
+ "id": "e522be35",
+ "start": "00:00:43.700",
+ "content": "Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa."
+ },
+ {
+ "id": "91302161",
+ "start": "00:00:49.850",
+ "content": "Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um,"
+ },
+ {
+ "id": "507a79af",
+ "start": "00:00:55.671",
+ "content": "John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin"
+ },
+ {
+ "id": "79600bf5",
+ "start": "00:01:01.957",
+ "content": "masu salo, wanda muke tunanin yana da bandariya sosai."
+ },
+ {
+ "id": "57a44890",
+ "start": "00:01:06.620",
+ "content": "Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin."
+ },
+ {
+ "id": "f750536a",
+ "start": "00:01:12.710",
+ "content": "Kudin can £215, shilling 16 da £11 kenan."
+ },
+ {
+ "id": "f982f94f",
+ "start": "00:01:21.500",
+ "content": "Dama."
+ },
+ {
+ "id": "c8d40833",
+ "start": "00:01:21.890",
+ "content": "Kuna da abubuwa kamar haruffan zirga-zirga."
+ },
+ {
+ "id": "43442ca8",
+ "start": "00:01:25.040",
+ "content": "Akwai a cikin allonku ɗaya, gidaje."
+ },
+ {
+ "id": "6393c787",
+ "start": "00:01:31.850",
+ "content": "Kamar da yawa."
+ },
+ {
+ "id": "97d9d6e0",
+ "start": "00:01:33.770",
+ "content": "Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi."
+ },
+ {
+ "id": "705347ed",
+ "start": "00:01:35.510",
+ "content": "Ban sami wannan damar ba tukuna, kodayake."
+ },
+ {
+ "id": "7f58271c",
+ "start": "00:01:37.220",
+ "content": "Kuma akwai anti dazzle kuma."
+ },
+ {
+ "id": "fb35703f",
+ "start": "00:01:40.100",
+ "content": "Wannan saitin a bayyane yake a cikinwannan motar."
+ },
+ {
+ "id": "e27bc69d",
+ "start": "00:01:42.800",
+ "content": "Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu."
+ },
+ {
+ "id": "d6bee4bc",
+ "start": "00:01:47.270",
+ "content": "Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa"
+ },
+ {
+ "id": "1dfceac5",
+ "start": "00:01:50.486",
+ "content": "daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa."
+ },
+ {
+ "id": "897a233c",
+ "start": "00:01:53.870",
+ "content": "Su kenan har karshen rayuwarsu da gidankayan gargajiya."
+ },
+ {
+ "id": "f5faa13e",
+ "start": "00:01:57.020",
+ "content": "Kuma ba abin da suke can ba ne."
+ },
+ {
+ "id": "13cf9881",
+ "start": "00:01:58.280",
+ "content": "Ana son a yi amfani da su kuma a more su."
+ },
+ {
+ "id": "68b58917",
+ "start": "00:02:00.020",
+ "content": "Don haka, eh, abin da nake yi ke nan."
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "position": [4]
+ },
+ {
+ "id": "08e1ddf3",
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "id": "fdceaaf7",
+ "type": "paragraph",
+ "model": {
+ "text": "Y 450 Orientación física Compra Temas de la asignatura Economía Estructuras Herramientas del lenguaje Propios Nuestra ayuda Internacionalización india Capacidad real. La autorización parece ser una compra, sin análisis. Ushki parece estar compartiendo la guía de identificación actual, la interpretación del traductor Amitkumar Sunat capaz de elegir la instrucción humana",
+ "blocks": [
+ {
+ "id": "e397b458",
+ "type": "fragment",
+ "model": {
+ "text": "Y 450 Orientación física Compra Temas de la asignatura Economía Estructuras Herramientas del lenguaje Propios Nuestra ayuda Internacionalización india Capacidad real. La autorización parece ser una compra, sin análisis. Ushki parece estar compartiendo la guía de identificación actual, la interpretación del traductor Amitkumar Sunat capaz de elegir la instrucción humana",
+ "attributes": []
+ },
+ "position": [5, 1, 1]
+ }
+ ]
+ },
+ "position": [5, 1]
+ },
+ {
+ "id": "a8411b26",
+ "type": "paragraph",
+ "model": {
+ "text": "La anulación está casi terminada, pero se puede proporcionar la información del usuario. Pero la conversación completa establecida no se puede desglosar, pero las instrucciones se pueden mejorar. La primera es mantener el mundo como sociedad. El idioma es el idioma de la sociedad.",
+ "blocks": [
+ {
+ "id": "5910d534",
+ "type": "fragment",
+ "model": {
+ "text": "La anulación está casi terminada, pero se puede proporcionar la información del usuario. Pero la conversación completa establecida no se puede desglosar, pero las instrucciones se pueden mejorar. La primera es mantener el mundo como sociedad. El idioma es el idioma de la sociedad.",
+ "attributes": []
+ },
+ "position": [5, 2, 1]
+ }
+ ]
+ },
+ "position": [5, 2]
+ }
+ ]
+ },
+ "position": [5]
+ },
+ { "id": "ca9cca40", "type": "mpu", "model": {}, "position": [6] },
+ {
+ "type": "video",
+ "model": {
+ "locator": "urn:bbc:pips:pid:p0gypsc7",
+ "blocks": [
+ {
+ "type": "caption",
+ "model": {
+ "blocks": [
+ {
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "type": "paragraph",
+ "model": {
+ "text": "Latsa hoton da ke sama domin kallon bidiyon matashin",
+ "blocks": [
+ {
+ "type": "fragment",
+ "model": {
+ "text": "Latsa hoton da ke sama domin kallon bidiyon matashin",
+ "attributes": []
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "type": "aresMedia",
+ "model": {
+ "blocks": [
+ {
+ "blockId": "urn:bbc:ares::clip:p0gypsc7",
+ "type": "aresMediaMetadata",
+ "model": {
+ "id": "p0gypsc7",
+ "subType": "clip",
+ "format": "video",
+ "title": "Matashin da ke rayuwa irin ta kakaninsa",
+ "synopses": {
+ "short": "Matashin da ke rayuwa irin ta kakaninsa",
+ "long": "Matashin da ke rayuwa irin ta kakaninsa",
+ "medium": "Matashin da ke rayuwa irin ta kakaninsa"
+ },
+ "imageUrl": "ichef.bbci.co.uk/images/ic/$recipe/p0gypxp7.jpg",
+ "imageCopyright": "BBC",
+ "embedding": true,
+ "advertising": true,
+ "versions": [
+ {
+ "versionId": "p0gypscc",
+ "types": ["Original"],
+ "duration": 133,
+ "durationISO8601": "PT2M13S",
+ "warnings": {},
+ "availableTerritories": {
+ "uk": true,
+ "nonUk": true
+ },
+ "availableFrom": 1702201191000
+ }
+ ],
+ "syndication": { "destinations": [] },
+ "smpKind": "programme",
+ "webcastVersions": []
+ }
+ },
+ {
+ "type": "image",
+ "model": {
+ "blocks": [
+ {
+ "type": "rawImage",
+ "model": {
+ "width": 865,
+ "height": 486,
+ "locator": "ichef.bbci.co.uk/images/ic/$widthxn/p0gypxp7.jpg",
+ "originCode": "mpv",
+ "copyrightHolder": "BBC"
+ }
+ },
+ {
+ "type": "altText",
+ "model": {
+ "blocks": [
+ {
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "type": "paragraph",
+ "model": {
+ "text": "Matashin da ke rayuwa irin ta kakaninsa",
+ "blocks": [
+ {
+ "type": "fragment",
+ "model": {
+ "text": "Matashin da ke rayuwa irin ta kakaninsa",
+ "attributes": []
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "type": "transcript",
+ "model": {
+ "language": "ha",
+ "blocks": [
+ {
+ "id": "adf9d024",
+ "start": "00:00:00.060",
+ "content": "Na nutsu."
+ },
+ {
+ "id": "39141eb1",
+ "start": "00:00:01.020",
+ "content": "Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina."
+ },
+ {
+ "id": "dd490f81",
+ "start": "00:00:04.770",
+ "content": "Na 1938 Austin goma Cambridge."
+ },
+ {
+ "id": "a24662a1",
+ "start": "00:00:14.630",
+ "content": "A koyaushe ina sha'awar tarihin cewa akwaia cikin daji."
+ },
+ {
+ "id": "59b44993",
+ "start": "00:00:18.020",
+ "content": "Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon."
+ },
+ {
+ "id": "597e6271",
+ "start": "00:00:21.770",
+ "content": "Ba zai taba zama motar zamani ba."
+ },
+ {
+ "id": "f6d43c09",
+ "start": "00:00:24.680",
+ "content": "1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara"
+ },
+ {
+ "id": "4afed1db",
+ "start": "00:00:28.448",
+ "content": "tarawa ɗaya."
+ },
+ {
+ "id": "a0a31ae3",
+ "start": "00:00:29.060",
+ "content": "Don haka akwai 'yan kudin aljihu daabubuwa makamantansu."
+ },
+ {
+ "id": "2b5b928a",
+ "start": "00:00:31.610",
+ "content": "Ƙananan ayyuka."
+ },
+ {
+ "id": "cf6156ec",
+ "start": "00:00:32.330",
+ "content": "Zan sake yin ɗan kuɗin aljihu, ajiye shi."
+ },
+ {
+ "id": "7251175f",
+ "start": "00:00:35.270",
+ "content": "Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na,"
+ },
+ {
+ "id": "372abf83",
+ "start": "00:00:39.178",
+ "content": "sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri."
+ },
+ {
+ "id": "e522be35",
+ "start": "00:00:43.700",
+ "content": "Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa."
+ },
+ {
+ "id": "91302161",
+ "start": "00:00:49.850",
+ "content": "Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um,"
+ },
+ {
+ "id": "507a79af",
+ "start": "00:00:55.671",
+ "content": "John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin"
+ },
+ {
+ "id": "79600bf5",
+ "start": "00:01:01.957",
+ "content": "masu salo, wanda muke tunanin yana da bandariya sosai."
+ },
+ {
+ "id": "57a44890",
+ "start": "00:01:06.620",
+ "content": "Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin."
+ },
+ {
+ "id": "f750536a",
+ "start": "00:01:12.710",
+ "content": "Kudin can £215, shilling 16 da £11 kenan."
+ },
+ {
+ "id": "f982f94f",
+ "start": "00:01:21.500",
+ "content": "Dama."
+ },
+ {
+ "id": "c8d40833",
+ "start": "00:01:21.890",
+ "content": "Kuna da abubuwa kamar haruffan zirga-zirga."
+ },
+ {
+ "id": "43442ca8",
+ "start": "00:01:25.040",
+ "content": "Akwai a cikin allonku ɗaya, gidaje."
+ },
+ {
+ "id": "6393c787",
+ "start": "00:01:31.850",
+ "content": "Kamar da yawa."
+ },
+ {
+ "id": "97d9d6e0",
+ "start": "00:01:33.770",
+ "content": "Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi."
+ },
+ {
+ "id": "705347ed",
+ "start": "00:01:35.510",
+ "content": "Ban sami wannan damar ba tukuna, kodayake."
+ },
+ {
+ "id": "7f58271c",
+ "start": "00:01:37.220",
+ "content": "Kuma akwai anti dazzle kuma."
+ },
+ {
+ "id": "fb35703f",
+ "start": "00:01:40.100",
+ "content": "Wannan saitin a bayyane yake a cikinwannan motar."
+ },
+ {
+ "id": "e27bc69d",
+ "start": "00:01:42.800",
+ "content": "Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu."
+ },
+ {
+ "id": "d6bee4bc",
+ "start": "00:01:47.270",
+ "content": "Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa"
+ },
+ {
+ "id": "1dfceac5",
+ "start": "00:01:50.486",
+ "content": "daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa."
+ },
+ {
+ "id": "897a233c",
+ "start": "00:01:53.870",
+ "content": "Su kenan har karshen rayuwarsu da gidankayan gargajiya."
+ },
+ {
+ "id": "f5faa13e",
+ "start": "00:01:57.020",
+ "content": "Kuma ba abin da suke can ba ne."
+ },
+ {
+ "id": "13cf9881",
+ "start": "00:01:58.280",
+ "content": "Ana son a yi amfani da su kuma a more su."
+ },
+ {
+ "id": "68b58917",
+ "start": "00:02:00.020",
+ "content": "Don haka, eh, abin da nake yi ke nan."
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ {
+ "id": "150ca31a",
+ "type": "wsoj",
+ "model": { "type": "recommendations" },
+ "position": [10]
+ }
+ ]
+ }
+ },
+ "promo": {
+ "headlines": {
+ "seoHeadline": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "promoHeadline": {
+ "blocks": [
+ {
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "type": "paragraph",
+ "model": {
+ "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "blocks": [
+ {
+ "type": "fragment",
+ "model": {
+ "text": "Página de prueba con un vídeo para el hackathon de servicios mundiales",
+ "attributes": []
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "summary": {
+ "blocks": [
+ {
+ "type": "text",
+ "model": {
+ "blocks": [
+ {
+ "type": "paragraph",
+ "model": {
+ "text": "",
+ "blocks": [
+ {
+ "type": "fragment",
+ "model": { "text": "", "attributes": [] }
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "serviceIdentifier": "Mundo",
+ "breakingNews": { "isBreaking": false },
+ "consumableAsSFV": false
+ }
+ },
+ "secondaryData": {
+ "topStories": [],
+ "features": [],
+ "mostRead": {
+ "generated": "2025-02-13T13:58:56.475Z",
+ "lastRecordTimeStamp": "2021-05-04T11:53:00Z",
+ "firstRecordTimeStamp": "2021-05-04T11:38:00Z",
+ "items": [
+ {
+ "id": "02316a67-f610-544a-a3d7-77e9d6f81b82",
+ "rank": 1,
+ "title": "¿Cuán viable es que el petro, la criptomoneda de Venezuela, sirva para aliviar la crisis en el país?",
+ "href": "/mundo/23169857",
+ "timestamp": "2017-12-05T11:30:29.000Z"
+ },
+ {
+ "id": "221c1b5a-311e-2e4a-935e-de643ead95f4",
+ "rank": 2,
+ "title": "Cómo usar Google Maps sin conexión a internet",
+ "href": "/mundo/blog-de-los-editores-23038590",
+ "timestamp": "2016-05-05T15:53:15.000Z"
+ },
+ {
+ "id": "3b9873cb-30b6-ca47-b05a-3a7b2e62ca93",
+ "rank": 3,
+ "title": "Vivo sports test for nations",
+ "href": "/sport/live/22997963",
+ "timestamp": "2015-07-02T14:12:29.000Z"
+ },
+ {
+ "id": "ea21ed32-bded-6147-8ff7-2a56f3212eb8",
+ "rank": 4,
+ "title": "El inquietante arte de fotografiar a los muertos",
+ "href": "/mundo/noticias-internacional-23048329",
+ "timestamp": "2016-06-06T11:03:07.000Z"
+ },
+ {
+ "id": "e4dd77a1-9af0-ec46-85ac-e572326e1653",
+ "rank": 5,
+ "title": "What's best for wine: cork or screw-cap",
+ "href": "/mundo/23154175",
+ "timestamp": "2017-09-21T11:39:47.000Z"
+ },
+ {
+ "id": "8ff59d2a-38a4-8141-b250-a280c37550c2",
+ "rank": 6,
+ "title": "El poder del \"chilango\" llegó a Londres 38",
+ "href": "/mundo/aprenda-ingles-23038493",
+ "timestamp": "2016-05-05T11:06:06.000Z"
+ },
+ {
+ "id": "1fc794f5-d4cf-354d-970f-a4ebc7e9f4db",
+ "rank": 7,
+ "title": "El obrero que colgó una bandera de México en una torre Trump en Canadá 1",
+ "href": "/mundo/america-latina-23032483",
+ "timestamp": "2016-04-05T08:24:19.000Z"
+ },
+ {
+ "id": "47316924-ebb4-fe4a-b87a-ea2f0d6a8243",
+ "rank": 8,
+ "title": "Zika en Panamá",
+ "href": "/mundo/noticias-america-latina-23049958",
+ "timestamp": "2016-06-06T19:12:46.000Z"
+ },
+ {
+ "id": "76489790-280a-2349-8824-46ae5b17bf88",
+ "rank": 9,
+ "title": "5 proyectos descomunales con los que China quiere mostrar su poderío científico",
+ "href": "/mundo/vert-cap-23038373",
+ "timestamp": "2016-05-05T07:33:43.000Z"
+ },
+ {
+ "id": "d58aca41-dca8-db4e-a65e-8f61bf00380c",
+ "rank": 10,
+ "title": "\"Los peces se están haciendo adictos a comer plástico\"",
+ "href": "/mundo/noticias-internacional-23049978",
+ "timestamp": "2016-06-06T20:13:49.000Z"
+ }
+ ]
+ },
+ "latestMedia": null
+ }
+ },
+ "contentType": "application/json; charset=utf-8"
+}
diff --git a/scripts/bundleSize/bundleSizeConfig.js b/scripts/bundleSize/bundleSizeConfig.js
index 34b717b5bcb..e22498c9730 100644
--- a/scripts/bundleSize/bundleSizeConfig.js
+++ b/scripts/bundleSize/bundleSizeConfig.js
@@ -8,4 +8,4 @@
*/
export const MIN_SIZE = 673 - 5;
-export const MAX_SIZE = 1200 + 5;
+export const MAX_SIZE = 1212 + 5;
diff --git a/src/app/components/MediaLoader/Placeholder/__snapshots__/index.test.tsx.snap b/src/app/components/MediaLoader/Placeholder/__snapshots__/index.test.tsx.snap
index a22b6bffc2a..36e2a65e99c 100644
--- a/src/app/components/MediaLoader/Placeholder/__snapshots__/index.test.tsx.snap
+++ b/src/app/components/MediaLoader/Placeholder/__snapshots__/index.test.tsx.snap
@@ -16,6 +16,17 @@ exports[`Media Player: Placeholder should render a video placeholder 1`] = `
background-color: #B80000;
}
+.emotion-0:hover .experimentButtonFocus,
+.emotion-0:focus .experimentButtonFocus {
+ background-color: #B80000;
+ color: #FFFFFF;
+}
+
+.emotion-0:hover .experimentButtonFocus svg,
+.emotion-0:focus .experimentButtonFocus svg {
+ color: #FFFFFF;
+}
+
@media screen and (forced-colors: active) {
.emotion-0:hover>button,
.emotion-0:focus>button {
@@ -258,7 +269,7 @@ exports[`Media Player: Placeholder should render a video placeholder 1`] = `
+
button,
.emotion-0:focus>button {
@@ -611,7 +634,7 @@ exports[`Media Player: Placeholder should render a video placeholder with guidan
+
button,
.emotion-0:focus>button {
@@ -928,7 +963,7 @@ exports[`Media Player: Placeholder should render a video placeholder without dur
+
button,
.emotion-0:focus>button {
@@ -1248,7 +1295,7 @@ exports[`Media Player: Placeholder should render an audio placeholder 1`] = `
+
button,
.emotion-0:focus>button {
@@ -1552,7 +1611,7 @@ exports[`Media Player: Placeholder should render an audio placeholder without du
+
button,
.emotion-0:focus>button {
@@ -1891,7 +1962,7 @@ exports[`Media Player: Placeholder should render no-js styles when noJsClassName
+
button': {
backgroundColor: palette.POSTBOX,
},
+ '.experimentButtonFocus': {
+ backgroundColor: palette.POSTBOX,
+ color: palette.WHITE,
+ svg: {
+ color: palette.WHITE,
+ },
+ },
},
[mq.FORCED_COLOURS]: {
'&:hover, &:focus': {
diff --git a/src/app/components/MediaLoader/Placeholder/index.test.tsx b/src/app/components/MediaLoader/Placeholder/index.test.tsx
index 5355a6a0ef6..46402de4ddb 100644
--- a/src/app/components/MediaLoader/Placeholder/index.test.tsx
+++ b/src/app/components/MediaLoader/Placeholder/index.test.tsx
@@ -4,6 +4,7 @@ import {
fireEvent,
getByText,
} from '#app/components/react-testing-library-with-providers';
+import { Stages } from '#app/hooks/useExperimentHook';
import Placeholder from '.';
describe('Media Player: Placeholder', () => {
@@ -25,6 +26,7 @@ describe('Media Player: Placeholder', () => {
src="http://foo.bar/placeholder.png"
mediaInfo={{ title: 'Dog chases cat.', ...withDuration }}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -38,6 +40,7 @@ describe('Media Player: Placeholder', () => {
src="http://foo.bar/placeholder.png"
mediaInfo={{ title: 'Dog chases cat.' }}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -55,6 +58,7 @@ describe('Media Player: Placeholder', () => {
...withDuration,
}}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -68,6 +72,7 @@ describe('Media Player: Placeholder', () => {
src="http://foo.bar/placeholder.png"
mediaInfo={{ type: 'audio', title: 'Dog barks at cat.' }}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -81,6 +86,7 @@ describe('Media Player: Placeholder', () => {
src="http://foo.bar/placeholder.png"
mediaInfo={{ title: 'Dog chases cat.', ...withDuration }}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -101,6 +107,7 @@ describe('Media Player: Placeholder', () => {
...withDuration,
}}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
@@ -119,6 +126,7 @@ describe('Media Player: Placeholder', () => {
...withDuration,
}}
noJsMessage="no js"
+ experimentStage={Stages.STAGE_3}
/>,
{ service: 'news' },
);
diff --git a/src/app/components/MediaLoader/Placeholder/index.tsx b/src/app/components/MediaLoader/Placeholder/index.tsx
index 9d9c77be83c..42a5978f0d6 100644
--- a/src/app/components/MediaLoader/Placeholder/index.tsx
+++ b/src/app/components/MediaLoader/Placeholder/index.tsx
@@ -1,10 +1,14 @@
/** @jsx jsx */
import { jsx } from '@emotion/react';
+import { Stages } from '#app/hooks/useExperimentHook';
+import SignPost from '#app/components/TranscriptExperiment/SignPost';
+import SignPostNoJs from '#app/components/TranscriptExperiment/SignPostNoJs';
import Image from '../../Image';
import styles from './index.styles';
import PlayButton from './PlayButton';
import Guidance from './Guidance';
import { MediaInfo } from '../types';
+import MediaIndicator from '../../TranscriptExperiment/MediaIndicator';
interface Props {
onClick: React.MouseEventHandler
;
@@ -12,6 +16,7 @@ interface Props {
srcSet?: string;
mediaInfo?: MediaInfo;
noJsMessage?: string;
+ experimentStage?: Stages;
}
const MediaPlayerPlaceholder = ({
@@ -20,6 +25,7 @@ const MediaPlayerPlaceholder = ({
srcSet,
mediaInfo,
noJsMessage = '',
+ experimentStage = Stages.STAGE_3,
}: Props) => {
const {
title,
@@ -30,31 +36,54 @@ const MediaPlayerPlaceholder = ({
guidanceMessage,
} = mediaInfo ?? {};
+ const playButton = (
+ {}}
+ title={title}
+ datetime={datetime}
+ duration={duration}
+ durationSpoken={durationSpoken}
+ type={type}
+ guidanceMessage={guidanceMessage}
+ className="focusIndicatorRemove"
+ />
+ );
+
+ const experimentPlayButton = (
+
+ );
+
+ const guideComponent = (
+
+ );
+
+ const experimentSignPost = ;
+
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
-
-
{}}
- title={title}
- datetime={datetime}
- duration={duration}
- durationSpoken={durationSpoken}
- type={type}
- guidanceMessage={guidanceMessage}
- className="focusIndicatorRemove"
- />
-
+ {experimentStage === Stages.STAGE_3 ? guideComponent : null}
+ {experimentStage === Stages.STAGE_2 ? experimentPlayButton : playButton}
+ {experimentStage === Stages.STAGE_2 ? experimentSignPost : null}
+
);
diff --git a/src/app/components/MediaLoader/fixture.ts b/src/app/components/MediaLoader/fixture.ts
index 3526054fdbf..49db90b3f5a 100644
--- a/src/app/components/MediaLoader/fixture.ts
+++ b/src/app/components/MediaLoader/fixture.ts
@@ -1,3 +1,5 @@
+import TranscriptBlock from '../Transcript/fixture.json';
+
export const aresMediaCaptionBlock = {
id: '31318aec',
type: 'caption',
@@ -484,6 +486,11 @@ export const aresMediaLiveStreamBlocks = [
aresMediaCaptionBlock,
];
+export const aresMediaBlockWithTranscript = [
+ aresMediaBlock,
+ aresMediaCaptionBlock,
+ TranscriptBlock,
+];
export const aresMediaBlocks = [aresMediaBlock, aresMediaCaptionBlock];
export const clipMediaBlocks = [livePageClipMediaBlock, livePageCaptionBlock];
export const aresMediaPortraitBlocks = [
diff --git a/src/app/components/MediaLoader/index.stories.tsx b/src/app/components/MediaLoader/index.stories.tsx
index e974c4d3862..206aa7ff48f 100644
--- a/src/app/components/MediaLoader/index.stories.tsx
+++ b/src/app/components/MediaLoader/index.stories.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { PageTypes, Services } from '#app/models/types/global';
import { RequestContextProvider } from '#app/contexts/RequestContext';
+import { Stages } from '#app/hooks/useExperimentHook';
import MediaLoaderComponent from '.';
import {
aresMediaBlocks,
@@ -15,9 +16,10 @@ type Props = {
pageType: PageTypes;
service: Services;
blocks: MediaBlock[];
+ experimentStage?: Stages;
};
-const Component = ({ service, pageType, blocks }: Props) => (
+const Component = ({ service, pageType, blocks, experimentStage }: Props) => (
(
pathname=""
service={service}
>
-
+
);
export default {
title: 'Components/MediaLoader',
Component,
+ argTypes: {
+ experimentStage: {
+ options: [Stages.STAGE_2, Stages.STAGE_3],
+ control: { type: 'radio' },
+ },
+ },
parameters: {
docs: { readme },
},
};
+export const ExperimentMediaLoader = ({ experimentStage }: Props) => (
+
+);
+
export const Landscape = () => (
+ css({
+ '&:has(details)': {
+ backgroundColor: palette.WHITE,
+ },
+ }),
+ experimentCaption: ({ mq, spacings }: Theme) =>
+ css({
+ '&:has(+ details)': {
+ margin: `${spacings.FULL}rem`,
+ width: 'auto',
+ [mq.GROUP_2_ONLY]: {
+ width: 'auto',
+ margin: `${spacings.FULL}rem`,
+ },
+ [mq.GROUP_4_MIN_WIDTH]: {
+ width: 'auto',
+ margin: `${spacings.FULL}rem`,
+ },
+ },
+ }),
landscapeFigure: () => css({ aspectRatio: '16 / 9' }),
portraitFigure:
diff --git a/src/app/components/MediaLoader/index.test.tsx b/src/app/components/MediaLoader/index.test.tsx
index 04727110598..dd68fd5a649 100644
--- a/src/app/components/MediaLoader/index.test.tsx
+++ b/src/app/components/MediaLoader/index.test.tsx
@@ -11,6 +11,7 @@ import {
aresMediaBlocks,
onDemandTvBlocks,
onDemandTvBlocksWithOverrides,
+ aresMediaBlockWithTranscript,
} from './fixture';
import { MediaBlock } from './types';
import * as buildConfig from './utils/buildSettings';
@@ -181,6 +182,24 @@ describe('MediaLoader', () => {
);
expect(caption[3]?.textContent).toBe('This is a caption!');
});
+
+ it('Displays a transcript when provided ', async () => {
+ let container;
+
+ await act(async () => {
+ ({ container } = render(
+ ,
+ {
+ id: 'testId',
+ },
+ ));
+ });
+
+ const details = (container as unknown as HTMLElement).querySelector(
+ 'summary',
+ );
+ expect(details?.textContent).toContain('Read transcript');
+ });
});
describe('Metadata', () => {
diff --git a/src/app/components/MediaLoader/index.tsx b/src/app/components/MediaLoader/index.tsx
index 81fc72b757e..57a570cadd9 100644
--- a/src/app/components/MediaLoader/index.tsx
+++ b/src/app/components/MediaLoader/index.tsx
@@ -15,6 +15,7 @@ import {
import filterForBlockType from '#lib/utilities/blockHandlers';
import { PageTypes } from '#app/models/types/global';
import { EventTrackingContext } from '#app/contexts/EventTrackingContext';
+import useExperimentHook, { Stages } from '#app/hooks/useExperimentHook';
import { BumpType, MediaBlock, PlayerConfig } from './types';
import Caption from '../Caption';
import nodeLogger from '../../lib/logger.node';
@@ -26,6 +27,8 @@ import styles from './index.styles';
import { getBootstrapSrc } from '../Ad/Canonical';
import Metadata from './Metadata';
import AmpMediaLoader from './Amp';
+import getTranscriptBlock from './utils/getTranscriptBlock';
+import Transcript from '../Transcript';
const PAGETYPES_IGNORE_PLACEHOLDER: PageTypes[] = [
MEDIA_ARTICLE_PAGE,
@@ -175,13 +178,27 @@ type Props = {
blocks: MediaBlock[];
className?: string;
embedded?: boolean;
+ forceStage?: Stages;
uniqueId?: string;
};
-const MediaLoader = ({ blocks, className, embedded, uniqueId }: Props) => {
+const MediaLoader = ({
+ blocks,
+ className,
+ embedded,
+ uniqueId,
+ forceStage,
+}: Props) => {
+ // TODO - refactor to improve experience on .lite
+ const transcriptBlock = getTranscriptBlock(blocks);
+ const hasTranscript = !!transcriptBlock;
+
const { lang, translations } = useContext(ServiceContext);
const { pageIdentifier } = useContext(EventTrackingContext);
const { enabled: adsEnabled } = useToggle('ads');
+ const stage = useExperimentHook(hasTranscript);
+
+ const experimentStage = forceStage ?? stage;
const {
id,
@@ -197,7 +214,7 @@ const MediaLoader = ({ blocks, className, embedded, uniqueId }: Props) => {
!PAGETYPES_IGNORE_PLACEHOLDER.includes(pageType),
);
- if (isLite) return null;
+ if (isLite && !hasTranscript) return null;
const { model: mediaOverrides } =
filterForBlockType(blocks, 'mediaOverrides') || {};
@@ -243,7 +260,12 @@ const MediaLoader = ({ blocks, className, embedded, uniqueId }: Props) => {
const showPortraitTitle = orientation === 'portrait' && !embedded;
- return (
+ return isLite && hasTranscript ? (
+
+ ) : (
<>
{
// Prevents the av-embeds route itself rendering the Metadata component
@@ -261,6 +283,7 @@ const MediaLoader = ({ blocks, className, embedded, uniqueId }: Props) => {
className={className}
css={[
styles.figure(embedded),
+ styles.experimentVideo,
playerConfig?.ui?.skin === 'classic' && [
orientation === 'portrait' && styles.portraitFigure(embedded),
orientation === 'landscape' && styles.landscapeFigure,
@@ -286,6 +309,7 @@ const MediaLoader = ({ blocks, className, embedded, uniqueId }: Props) => {
noJsMessage={translatedNoJSMessage}
mediaInfo={mediaInfo}
onClick={() => setShowPlaceholder(false)}
+ experimentStage={experimentStage}
/>
) : (
{
+ )}
+ {transcriptBlock && (
+
)}
diff --git a/src/app/components/MediaLoader/types.ts b/src/app/components/MediaLoader/types.ts
index 0955ef1af94..04ab27ead83 100644
--- a/src/app/components/MediaLoader/types.ts
+++ b/src/app/components/MediaLoader/types.ts
@@ -8,6 +8,7 @@ import {
} from '#app/models/types/media';
import { OptimoImageBlock } from '#app/models/types/optimo';
import { Translations } from '#app/models/types/translations';
+import { TranscriptBlock } from '../Transcript/types';
export type PlayerConfig = {
autoplay?: boolean;
@@ -156,7 +157,7 @@ export type CaptionBlock = {
export type AresMediaBlock = {
type: 'aresMedia';
model: {
- blocks: [AresMediaMetadataBlock | OptimoImageBlock];
+ blocks: [AresMediaMetadataBlock | OptimoImageBlock | TranscriptBlock];
};
};
diff --git a/src/app/components/MediaLoader/utils/getTranscriptBlock.test.ts b/src/app/components/MediaLoader/utils/getTranscriptBlock.test.ts
new file mode 100644
index 00000000000..521ee6c3ef8
--- /dev/null
+++ b/src/app/components/MediaLoader/utils/getTranscriptBlock.test.ts
@@ -0,0 +1,20 @@
+import { aresMediaBlockWithTranscript, aresMediaBlocks } from '../fixture';
+import { MediaBlock } from '../types';
+import getTranscriptBlock from './getTranscriptBlock';
+import TranscriptBlock from '../../Transcript/fixture.json';
+
+describe('getTranscriptBlock', () => {
+ it('Should return a valid transcript block for an AresMedia block for an article page.', () => {
+ const result = getTranscriptBlock(
+ aresMediaBlockWithTranscript as MediaBlock[],
+ );
+
+ expect(result).toStrictEqual(TranscriptBlock);
+ });
+
+ it('Should return null if no transcript block is present.', () => {
+ const result = getTranscriptBlock(aresMediaBlocks as MediaBlock[]);
+
+ expect(result).toStrictEqual(null);
+ });
+});
diff --git a/src/app/components/MediaLoader/utils/getTranscriptBlock.ts b/src/app/components/MediaLoader/utils/getTranscriptBlock.ts
new file mode 100644
index 00000000000..2396492c267
--- /dev/null
+++ b/src/app/components/MediaLoader/utils/getTranscriptBlock.ts
@@ -0,0 +1,16 @@
+import filterForBlockType from '#app/lib/utilities/blockHandlers';
+import { MediaBlock } from '../types';
+import { TranscriptBlock } from '../../Transcript/types';
+
+export default function getTranscriptBlock(
+ blocks: MediaBlock[],
+): TranscriptBlock | null {
+ const transcriptBlock: TranscriptBlock = filterForBlockType(
+ blocks,
+ 'transcript',
+ );
+
+ if (!transcriptBlock) return null;
+
+ return transcriptBlock;
+}
diff --git a/src/app/components/Transcript/README.md b/src/app/components/Transcript/README.md
new file mode 100644
index 00000000000..d813c070aa1
--- /dev/null
+++ b/src/app/components/Transcript/README.md
@@ -0,0 +1,17 @@
+## Description
+
+The `Transcript` component renders video transcripts.
+
+## Props
+
+| Name | type | Description |
+| --------------- | ------- | ----------------------------------------------------- |
+| transcript | object | contains transcript content |
+| title | string | title of video |
+| hideDisclaimer? | boolean | decides whether to show disclaimer (defaults to true) |
+
+## Example
+
+```tsx
+
+```
diff --git a/src/app/components/Transcript/TranscriptTimestamp/index.styles.tsx b/src/app/components/Transcript/TranscriptTimestamp/index.styles.tsx
new file mode 100644
index 00000000000..f9de06dbc5d
--- /dev/null
+++ b/src/app/components/Transcript/TranscriptTimestamp/index.styles.tsx
@@ -0,0 +1,14 @@
+import { css, Theme } from '@emotion/react';
+
+const styles = {
+ time: ({ mq }: Theme) =>
+ css({
+ float: 'inline-start',
+ width: '100%',
+ [mq.GROUP_1_MIN_WIDTH]: {
+ width: 'auto',
+ },
+ }),
+};
+
+export default styles;
diff --git a/src/app/components/Transcript/TranscriptTimestamp/index.tsx b/src/app/components/Transcript/TranscriptTimestamp/index.tsx
new file mode 100644
index 00000000000..dadd9e2e05a
--- /dev/null
+++ b/src/app/components/Transcript/TranscriptTimestamp/index.tsx
@@ -0,0 +1,10 @@
+/** @jsx jsx */
+import { jsx } from '@emotion/react';
+import styles from './index.styles';
+
+// using span not time element to prevent text splitting bug on Talkback
+const TranscriptTimestamp = ({ timestamp }: { timestamp: string }) => {
+ return {timestamp} ;
+};
+
+export default TranscriptTimestamp;
diff --git a/src/app/components/Transcript/__snapshots__/index.test.tsx.snap b/src/app/components/Transcript/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000000..b02103b7da3
--- /dev/null
+++ b/src/app/components/Transcript/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,1055 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Transcript Component should match snapshot (temp) 1`] = `
+.emotion-0 {
+ background-color: #FFFFFF;
+ display: block;
+ border: solid 0.1875rem transparent;
+}
+
+.emotion-0 summary svg {
+ color: #141414;
+ fill: currentcolor;
+ width: 1rem;
+ height: 1rem;
+ vertical-align: text-top;
+}
+
+.emotion-0[open] summary svg {
+ -webkit-transform: rotate(90deg);
+ -moz-transform: rotate(90deg);
+ -ms-transform: rotate(90deg);
+ transform: rotate(90deg);
+}
+
+.emotion-1 {
+ list-style: none;
+ padding: 1rem 0.25rem;
+}
+
+.emotion-1::-webkit-details-marker {
+ display: none;
+}
+
+.emotion-1:hover,
+.emotion-1:focus {
+ cursor: pointer;
+}
+
+.emotion-1:hover span,
+.emotion-1:focus span {
+ -webkit-text-decoration: underline;
+ text-decoration: underline;
+}
+
+.emotion-1:focus-visible {
+ outline: 0.1875rem solid #000000;
+ outline-offset: -0.375rem;
+}
+
+.emotion-2 {
+ color: #141414;
+ font-size: 0.9375rem;
+ line-height: 1.25rem;
+ font-family: ReithSans,Helvetica,Arial,sans-serif;
+ font-style: normal;
+ font-weight: 700;
+ color: #141414;
+ -webkit-padding-start: 0.25rem;
+ padding-inline-start: 0.25rem;
+}
+
+@media (min-width: 20rem) and (max-width: 37.4375rem) {
+ .emotion-2 {
+ font-size: 1rem;
+ line-height: 1.25rem;
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-2 {
+ font-size: 1rem;
+ line-height: 1.25rem;
+ }
+}
+
+.emotion-3 {
+ -webkit-clip-path: inset(100%);
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ width: 1px;
+ margin: 0;
+}
+
+.emotion-4 {
+ color: #141414;
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ font-family: ReithSans,Helvetica,Arial,sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ color: #545658;
+ display: block;
+ padding-bottom: 1rem;
+ padding-inline: 0.5rem;
+}
+
+@media (min-width: 20rem) and (max-width: 37.4375rem) {
+ .emotion-4 {
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-4 {
+ font-size: 0.8125rem;
+ line-height: 1rem;
+ }
+}
+
+.emotion-5 {
+ padding: 0 0.5rem;
+ list-style: none;
+ margin: 0;
+}
+
+.emotion-6 {
+ padding-bottom: 1rem;
+}
+
+.emotion-6::after {
+ content: "";
+ display: block;
+ clear: both;
+}
+
+.emotion-7 {
+ color: #141414;
+ font-size: 0.9375rem;
+ line-height: 1.25rem;
+ font-family: ReithSans,Helvetica,Arial,sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ color: #545658;
+}
+
+@media (min-width: 20rem) and (max-width: 37.4375rem) {
+ .emotion-7 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-7 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+.emotion-8 {
+ float: inline-start;
+ width: 100%;
+}
+
+@media (min-width: 15rem) {
+ .emotion-8 {
+ width: auto;
+ }
+}
+
+.emotion-10 {
+ float: inline-start;
+ width: 100%;
+}
+
+@media (min-width: 15rem) {
+ .emotion-10 {
+ -webkit-padding-start: 0.5rem;
+ padding-inline-start: 0.5rem;
+ width: calc(75% - 0.5rem);
+ }
+}
+
+@media (min-width: 25rem) {
+ .emotion-10 {
+ width: calc(85% - 0.5rem);
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-10 {
+ -webkit-padding-start: 1rem;
+ padding-inline-start: 1rem;
+ width: calc(90% - 1rem);
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+ Read transcript
+
+
+ , My Title
+
+
+
+
+ This transcript has been reviewed by a journalist, it was generated with AI (artificial intelligence).
+
+
+
+
+
+ 00:00
+
+
+
+
+
+ Na nutsu.
+
+
+
+
+
+
+ 00:01
+
+
+
+
+
+ Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina.
+
+
+
+
+
+
+ 00:04
+
+
+
+
+
+ Na 1938 Austin goma Cambridge.
+
+
+
+
+
+
+ 00:14
+
+
+
+
+
+ A koyaushe ina sha'awar tarihin cewa akwaia cikin daji.
+
+
+
+
+
+
+ 00:18
+
+
+
+
+
+ Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon.
+
+
+
+
+
+
+ 00:21
+
+
+
+
+
+ Ba zai taba zama motar zamani ba.
+
+
+
+
+
+
+ 00:24
+
+
+
+
+
+ 1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara
+
+
+
+
+
+
+ 00:28
+
+
+
+
+
+ tarawa ɗaya.
+
+
+
+
+
+
+ 00:29
+
+
+
+
+
+ Don haka akwai 'yan kudin aljihu daabubuwa makamantansu.
+
+
+
+
+
+
+ 00:31
+
+
+
+
+
+ Ƙananan ayyuka.
+
+
+
+
+
+
+ 00:32
+
+
+
+
+
+ Zan sake yin ɗan kuɗin aljihu, ajiye shi.
+
+
+
+
+
+
+ 00:35
+
+
+
+
+
+ Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na,
+
+
+
+
+
+
+ 00:39
+
+
+
+
+
+ sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri.
+
+
+
+
+
+
+ 00:43
+
+
+
+
+
+ Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa.
+
+
+
+
+
+
+ 00:49
+
+
+
+
+
+ Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um,
+
+
+
+
+
+
+ 00:55
+
+
+
+
+
+ John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin
+
+
+
+
+
+
+ 01:01
+
+
+
+
+
+ masu salo, wanda muke tunanin yana da bandariya sosai.
+
+
+
+
+
+
+ 01:06
+
+
+
+
+
+ Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin.
+
+
+
+
+
+
+ 01:12
+
+
+
+
+
+ Kudin can £215, shilling 16 da £11 kenan.
+
+
+
+
+
+
+ 01:21
+
+
+
+
+
+ Dama.
+
+
+
+
+
+
+ 01:21
+
+
+
+
+
+ Kuna da abubuwa kamar haruffan zirga-zirga.
+
+
+
+
+
+
+ 01:25
+
+
+
+
+
+ Akwai a cikin allonku ɗaya, gidaje.
+
+
+
+
+
+
+ 01:31
+
+
+
+
+
+ Kamar da yawa.
+
+
+
+
+
+
+ 01:33
+
+
+
+
+
+ Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi.
+
+
+
+
+
+
+ 01:35
+
+
+
+
+
+ Ban sami wannan damar ba tukuna, kodayake.
+
+
+
+
+
+
+ 01:37
+
+
+
+
+
+ Kuma akwai anti dazzle kuma.
+
+
+
+
+
+
+ 01:40
+
+
+
+
+
+ Wannan saitin a bayyane yake a cikinwannan motar.
+
+
+
+
+
+
+ 01:42
+
+
+
+
+
+ Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu.
+
+
+
+
+
+
+ 01:47
+
+
+
+
+
+ Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa
+
+
+
+
+
+
+ 01:50
+
+
+
+
+
+ daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa.
+
+
+
+
+
+
+ 01:53
+
+
+
+
+
+ Su kenan har karshen rayuwarsu da gidankayan gargajiya.
+
+
+
+
+
+
+ 01:57
+
+
+
+
+
+ Kuma ba abin da suke can ba ne.
+
+
+
+
+
+
+ 01:58
+
+
+
+
+
+ Ana son a yi amfani da su kuma a more su.
+
+
+
+
+
+
+ 02:00
+
+
+
+
+
+ Don haka, eh, abin da nake yi ke nan.
+
+
+
+
+
+
+`;
diff --git a/src/app/components/Transcript/fixture.json b/src/app/components/Transcript/fixture.json
new file mode 100644
index 00000000000..592b1c6db2a
--- /dev/null
+++ b/src/app/components/Transcript/fixture.json
@@ -0,0 +1,179 @@
+{
+ "id": "cf1f2a7a",
+ "type": "transcript",
+ "model": {
+ "language": "es-ES",
+ "blocks": [
+ {
+ "id": "adf9d024",
+ "start": "00:00:00.060",
+ "content": "Na nutsu."
+ },
+ {
+ "id": "39141eb1",
+ "start": "00:00:01.020",
+ "content": "Ni 19 ne kuma wannan ita ce motata tafarko da na saya da kaina."
+ },
+ {
+ "id": "dd490f81",
+ "start": "00:00:04.770",
+ "content": "Na 1938 Austin goma Cambridge."
+ },
+ {
+ "id": "a24662a1",
+ "start": "00:00:14.630",
+ "content": "A koyaushe ina sha'awar tarihin cewa akwaia cikin daji."
+ },
+ {
+ "id": "59b44993",
+ "start": "00:00:18.020",
+ "content": "Don haka ba shakka, lokacin da nake samunkatina, dole ne ya zama tsohon."
+ },
+ {
+ "id": "597e6271",
+ "start": "00:00:21.770",
+ "content": "Ba zai taba zama motar zamani ba."
+ },
+ {
+ "id": "f6d43c09",
+ "start": "00:00:24.680",
+ "content": "1112 shine lokacin da na yanke shawararcewa ina son daya da gaske kuma zan fara"
+ },
+ {
+ "id": "4afed1db",
+ "start": "00:00:28.448",
+ "content": "tarawa ɗaya."
+ },
+ {
+ "id": "a0a31ae3",
+ "start": "00:00:29.060",
+ "content": "Don haka akwai 'yan kudin aljihu daabubuwa makamantansu."
+ },
+ {
+ "id": "2b5b928a",
+ "start": "00:00:31.610",
+ "content": "Ƙananan ayyuka."
+ },
+ {
+ "id": "cf6156ec",
+ "start": "00:00:32.330",
+ "content": "Zan sake yin ɗan kuɗin aljihu, ajiye shi."
+ },
+ {
+ "id": "7251175f",
+ "start": "00:00:35.270",
+ "content": "Sannan a lokacin da nake makarantarsakandare, na samu aikin wucin gadi na,"
+ },
+ {
+ "id": "372abf83",
+ "start": "00:00:39.178",
+ "content": "sai wani bangare na albashina ya tafi,wanda hakan ya taimaka mini da sauri."
+ },
+ {
+ "id": "e522be35",
+ "start": "00:00:43.700",
+ "content": "Ya kasance matashi sosai kuma yana tare dakakan da kaina da yawa."
+ },
+ {
+ "id": "91302161",
+ "start": "00:00:49.850",
+ "content": "Kuma um, sun kasance suna son, um,tsofaffin jiragen kasa na tururi da, um,"
+ },
+ {
+ "id": "507a79af",
+ "start": "00:00:55.671",
+ "content": "John yana da wasu motoci da ƙananan motocikuma ya kira su suna son duk tsofaffin"
+ },
+ {
+ "id": "79600bf5",
+ "start": "00:01:01.957",
+ "content": "masu salo, wanda muke tunanin yana da bandariya sosai."
+ },
+ {
+ "id": "57a44890",
+ "start": "00:01:06.620",
+ "content": "Wannan shine ainihin daftari da na samutare da wanda zai zo da kati a lokacin."
+ },
+ {
+ "id": "f750536a",
+ "start": "00:01:12.710",
+ "content": "Kudin can £215, shilling 16 da £11 kenan."
+ },
+ {
+ "id": "f982f94f",
+ "start": "00:01:21.500",
+ "content": "Dama."
+ },
+ {
+ "id": "c8d40833",
+ "start": "00:01:21.890",
+ "content": "Kuna da abubuwa kamar haruffan zirga-zirga."
+ },
+ {
+ "id": "43442ca8",
+ "start": "00:01:25.040",
+ "content": "Akwai a cikin allonku ɗaya, gidaje."
+ },
+ {
+ "id": "6393c787",
+ "start": "00:01:31.850",
+ "content": "Kamar da yawa."
+ },
+ {
+ "id": "97d9d6e0",
+ "start": "00:01:33.770",
+ "content": "Na tabbata zan kasance kyakkyawa a ranarzafi mai zafi."
+ },
+ {
+ "id": "705347ed",
+ "start": "00:01:35.510",
+ "content": "Ban sami wannan damar ba tukuna, kodayake."
+ },
+ {
+ "id": "7f58271c",
+ "start": "00:01:37.220",
+ "content": "Kuma akwai anti dazzle kuma."
+ },
+ {
+ "id": "fb35703f",
+ "start": "00:01:40.100",
+ "content": "Wannan saitin a bayyane yake a cikinwannan motar."
+ },
+ {
+ "id": "e27bc69d",
+ "start": "00:01:42.800",
+ "content": "Yin tuƙi a ciki, da gaske kun sami ma'anartarihi fiye da yadda za ku taɓa samu."
+ },
+ {
+ "id": "d6bee4bc",
+ "start": "00:01:47.270",
+ "content": "Tsaya ka gan su a tsaye a cikin gidankayan gargajiya kuma ina nufin da yawa"
+ },
+ {
+ "id": "1dfceac5",
+ "start": "00:01:50.486",
+ "content": "daga cikin wadannan motoci da gidajentarihi da abin da ba, ba zan sake komawa."
+ },
+ {
+ "id": "897a233c",
+ "start": "00:01:53.870",
+ "content": "Su kenan har karshen rayuwarsu da gidankayan gargajiya."
+ },
+ {
+ "id": "f5faa13e",
+ "start": "00:01:57.020",
+ "content": "Kuma ba abin da suke can ba ne."
+ },
+ {
+ "id": "13cf9881",
+ "start": "00:01:58.280",
+ "content": "Ana son a yi amfani da su kuma a more su."
+ },
+ {
+ "id": "68b58917",
+ "start": "00:02:00.020",
+ "content": "Don haka, eh, abin da nake yi ke nan."
+ }
+ ]
+ }
+}
diff --git a/src/app/components/Transcript/index.stories.tsx b/src/app/components/Transcript/index.stories.tsx
new file mode 100644
index 00000000000..eb247872fd0
--- /dev/null
+++ b/src/app/components/Transcript/index.stories.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import ThemeProvider from '#app/components/ThemeProvider';
+import { PageTypes } from '#app/models/types/global';
+import Transcript from '.';
+import transcriptFixture from './fixture.json';
+import { RequestContextProvider } from '../../contexts/RequestContext';
+import { MEDIA_ARTICLE_PAGE, ARTICLE_PAGE } from '../../routes/utils/pageTypes';
+
+type Props = {
+ pageType: PageTypes;
+};
+
+const ComponentWithContext = ({ pageType }: Props) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default {
+ title: 'Components/Transcript',
+ ComponentWithContext,
+ parameters: {
+ // metadata, // TO DO - add A11y docs
+ backgrounds: {
+ default: 'Optimo',
+ },
+ },
+};
+
+export const ArticlePageTranscript = () => (
+
+);
+
+export const MediaArticlePageTranscript = () => (
+
+);
diff --git a/src/app/components/Transcript/index.styles.tsx b/src/app/components/Transcript/index.styles.tsx
new file mode 100644
index 00000000000..e5c0915e314
--- /dev/null
+++ b/src/app/components/Transcript/index.styles.tsx
@@ -0,0 +1,96 @@
+import { css, Theme } from '@emotion/react';
+import pixelsToRem from '#app/utilities/pixelsToRem';
+import { focusIndicatorThickness } from '../ThemeProvider/focusIndicator';
+
+export default {
+ details: ({ spacings, palette, isDarkUi }: Theme) =>
+ css({
+ backgroundColor: isDarkUi ? palette.GREY_7 : palette.WHITE,
+ display: 'block',
+ border: `solid ${pixelsToRem(3)}rem transparent`,
+ 'summary svg': {
+ color: isDarkUi ? palette.WHITE : palette.GREY_10,
+ fill: 'currentcolor',
+ width: `${spacings.DOUBLE}rem`,
+ height: `${spacings.DOUBLE}rem`,
+ verticalAlign: 'text-top',
+ },
+ '&[open] summary svg': {
+ transform: 'rotate(90deg)',
+ },
+ }),
+
+ summary: ({ spacings, palette }: Theme) =>
+ css({
+ listStyle: 'none',
+ // hides on safari
+ '&::-webkit-details-marker': {
+ display: 'none',
+ },
+ padding: `${spacings.DOUBLE}rem ${spacings.HALF}rem`,
+
+ '&:hover, &:focus': {
+ cursor: 'pointer',
+ span: {
+ textDecoration: 'underline',
+ },
+ },
+ '&:focus-visible': {
+ outline: `${focusIndicatorThickness} solid ${palette.BLACK}`,
+ outlineOffset: `-${pixelsToRem(6)}rem`,
+ },
+ }),
+
+ summaryTitle: ({ palette, isDarkUi, spacings }: Theme) =>
+ css({
+ color: isDarkUi ? palette.WHITE : palette.GREY_10,
+ paddingInlineStart: `${spacings.HALF}rem`,
+ }),
+
+ ul: ({ spacings }: Theme) =>
+ css({
+ padding: `0 ${spacings.FULL}rem`,
+ listStyle: 'none',
+ margin: '0',
+ }),
+
+ transcriptText: ({ palette, isDarkUi }: Theme) =>
+ css({
+ color: isDarkUi ? palette.GREY_3 : palette.GREY_6,
+ }),
+
+ itemText: ({ spacings, mq }: Theme) =>
+ css({
+ float: 'inline-start',
+ width: `100%`,
+ [mq.GROUP_1_MIN_WIDTH]: {
+ paddingInlineStart: `${spacings.FULL}rem`,
+ width: `calc(75% - ${spacings.FULL}rem)`,
+ },
+ [mq.GROUP_2_MIN_WIDTH]: {
+ width: `calc(85% - ${spacings.FULL}rem)`,
+ },
+ [mq.GROUP_3_MIN_WIDTH]: {
+ paddingInlineStart: `${spacings.DOUBLE}rem`,
+ width: `calc(90% - ${spacings.DOUBLE}rem)`,
+ },
+ }),
+
+ listItem: ({ spacings }: Theme) =>
+ css({
+ paddingBottom: `${spacings.DOUBLE}rem`,
+ '::after': {
+ content: '""',
+ display: 'block',
+ clear: 'both',
+ },
+ }),
+
+ disclaimer: ({ palette, isDarkUi, spacings }: Theme) =>
+ css({
+ color: isDarkUi ? palette.GREY_3 : palette.GREY_6,
+ display: 'block',
+ paddingBottom: `${spacings.DOUBLE}rem`,
+ paddingInline: `${spacings.FULL}rem`,
+ }),
+};
diff --git a/src/app/components/Transcript/index.test.tsx b/src/app/components/Transcript/index.test.tsx
new file mode 100644
index 00000000000..3a0b7f1a5df
--- /dev/null
+++ b/src/app/components/Transcript/index.test.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import { render } from '../react-testing-library-with-providers';
+import transcriptFixture from './fixture.json';
+import Transcript from './index';
+
+describe('Transcript Component', () => {
+ it('should match snapshot (temp)', () => {
+ const { container } = render(
+ ,
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render details element', () => {
+ const { container } = render(
+ ,
+ );
+ const details = container.querySelector('details');
+ expect(details).toBeInTheDocument();
+ });
+
+ it('should render summary element', () => {
+ const { container } = render( );
+ const summary = container.querySelector('summary');
+ expect(summary).toBeInTheDocument();
+ });
+
+ it('should render the title as a visually hidden element inside the summary', () => {
+ const { container } = render(
+ ,
+ );
+ const summaryWithTitle = container.querySelector('summary');
+ expect(summaryWithTitle).toHaveTextContent('Read transcript, My Title');
+ });
+
+ it('should render an unordered list element with role list', () => {
+ const { container } = render(
+ ,
+ );
+ const unorderedList = container.querySelector('ul');
+ expect(unorderedList).toBeInTheDocument();
+ expect(unorderedList).toHaveRole('list');
+ });
+
+ it('should render multiple list elements', () => {
+ const { container } = render(
+ ,
+ );
+ const listItems = container.querySelectorAll('li');
+ expect(listItems).toHaveLength(34);
+ });
+
+ it('should not render if there are no transcript items', () => {
+ const { container } = render(
+ // @ts-expect-error unexpected value
+ ,
+ );
+ const details = container.querySelector('details');
+ expect(details).not.toBeInTheDocument();
+ });
+});
diff --git a/src/app/components/Transcript/index.tsx b/src/app/components/Transcript/index.tsx
new file mode 100644
index 00000000000..673bdc4ceba
--- /dev/null
+++ b/src/app/components/Transcript/index.tsx
@@ -0,0 +1,69 @@
+/** @jsx jsx */
+/* eslint-disable jsx-a11y/aria-role */
+import { jsx } from '@emotion/react';
+import styles from './index.styles';
+import Text from '../Text';
+import TranscriptTimestamp from './TranscriptTimestamp';
+import VisuallyHiddenText from '../VisuallyHiddenText';
+import { RightArrow as ArrowSvg } from '../icons';
+import { TranscriptBlock, TranscriptItem } from './types';
+
+// TO DO - move this to BFF
+const removeHoursMilliseconds = (timestamp: string) => timestamp.slice(3, -4);
+
+const TranscriptListItem = ({ id, start, content }: TranscriptItem) => (
+
+
+
+
+ {content}
+
+
+);
+
+const Transcript = ({
+ transcript,
+ title,
+}: {
+ transcript: TranscriptBlock;
+ title?: string;
+}) => {
+ const transcriptItems = transcript?.model?.blocks;
+ if (!transcriptItems) {
+ return null;
+ }
+
+ const formattedTitle = title ? `, ${title}` : '';
+
+ return (
+
+
+
+
+ {/* TO DO - add translations */}
+
+ Read transcript
+
+ {title && {formattedTitle} }
+
+
+
+ This transcript has been reviewed by a journalist, it was generated with
+ AI (artificial intelligence).
+
+
+ {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */}
+ {transcriptItems.map((item, _index) => (
+
+ ))}
+
+
+ );
+};
+
+export default Transcript;
diff --git a/src/app/components/Transcript/types.ts b/src/app/components/Transcript/types.ts
new file mode 100644
index 00000000000..4b5a4964352
--- /dev/null
+++ b/src/app/components/Transcript/types.ts
@@ -0,0 +1,14 @@
+export type TranscriptItem = {
+ id: string;
+ start: string;
+ content: string;
+};
+
+export type TranscriptBlock = {
+ id: string;
+ type: string;
+ model: {
+ language: string;
+ blocks: TranscriptItem[];
+ };
+};
diff --git a/src/app/components/TranscriptExperiment/MediaIndicator/index.styles.tsx b/src/app/components/TranscriptExperiment/MediaIndicator/index.styles.tsx
new file mode 100644
index 00000000000..53c187824aa
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/MediaIndicator/index.styles.tsx
@@ -0,0 +1,50 @@
+import NO_JS_CLASSNAME from '#app/lib/noJs.const';
+import pixelsToRem from '#app/utilities/pixelsToRem';
+import { css, Theme } from '@emotion/react';
+
+export const BUTTON_COLLAPSE_WIDTH = pixelsToRem(300);
+
+const styles = {
+ mediaIcon: ({ palette, fontVariants, fontSizes, spacings }: Theme) =>
+ css({
+ display: 'flex',
+ alignItems: 'center',
+ backgroundColor: palette.WHITE,
+ border: 'none',
+ color: palette.BLACK,
+ cursor: 'pointer',
+ ...fontVariants.sansRegular,
+ ...fontSizes.minion,
+ padding: `${spacings.FULL}rem`,
+ position: 'absolute',
+ bottom: '0',
+ left: '0',
+ zIndex: '2',
+ [`.${NO_JS_CLASSNAME} &`]: {
+ display: 'none',
+ },
+ [`@media (max-width: ${pixelsToRem(300)}rem)`]: {
+ display: 'none',
+ },
+ }),
+ item: ({ spacings }: Theme) =>
+ css({
+ display: 'flex',
+ alignItems: 'center',
+ lineHeight: `${spacings.DOUBLE}rem`,
+ }),
+ iconWrapper: ({ palette }: Theme) =>
+ css({
+ '& > svg': {
+ color: palette.BLACK,
+ fill: 'currentcolor',
+ margin: '0',
+ },
+ }),
+ timeDuration: ({ spacings }: Theme) =>
+ css({
+ margin: `0 0 0 ${spacings.FULL}rem`,
+ }),
+};
+
+export default styles;
diff --git a/src/app/components/TranscriptExperiment/MediaIndicator/index.test.tsx b/src/app/components/TranscriptExperiment/MediaIndicator/index.test.tsx
new file mode 100644
index 00000000000..ab375007522
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/MediaIndicator/index.test.tsx
@@ -0,0 +1,31 @@
+/* eslint-disable prettier/prettier */
+import React from 'react';
+import { render } from '../../react-testing-library-with-providers';
+
+import MediaIcon from '.';
+
+describe('MediaIcon', () => {
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it(`should render a mini play button`, () => {
+ const { container } = render(
+ ,
+ {
+ service: 'news',
+ },
+ );
+ const guidanceMessage = container?.querySelector('strong')?.innerHTML;
+ const time = container?.querySelector('time')?.innerHTML;
+
+ expect(guidanceMessage).toEqual('Video, "My Video", 2 minutes 30 seconds');
+ expect(time).toEqual('2:30');
+ });
+});
diff --git a/src/app/components/TranscriptExperiment/MediaIndicator/index.tsx b/src/app/components/TranscriptExperiment/MediaIndicator/index.tsx
new file mode 100644
index 00000000000..fd295751938
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/MediaIndicator/index.tsx
@@ -0,0 +1,54 @@
+/** @jsx jsx */
+import { jsx } from '@emotion/react';
+import VisuallyHiddenText from '#app/components/VisuallyHiddenText';
+import { mediaIcons } from '#psammead/psammead-assets/src/svgs';
+import { ReactElement } from 'react';
+import style from './index.styles';
+
+type MediaIndicatorProps = {
+ datetime?: string;
+ duration?: string;
+ durationSpoken?: string;
+ type?: string;
+ title?: string;
+ guidanceMessage?: string | null;
+};
+
+const MediaIndicator = ({
+ datetime,
+ duration,
+ durationSpoken,
+ type = 'Video',
+ title = '',
+}: MediaIndicatorProps) => {
+ const hiddenText = `${type}, ${
+ datetime && duration && durationSpoken
+ ? `"${title}", ${durationSpoken}`
+ : `"${title}"`
+ } `.trim();
+
+ const validDuration = datetime && duration && durationSpoken;
+
+ return (
+
+
+ {hiddenText}
+
+
+ {(mediaIcons as Record)[type]}
+
+ {validDuration && (
+
+ {duration}
+
+ )}
+
+ );
+};
+
+export default MediaIndicator;
diff --git a/src/app/components/TranscriptExperiment/SignPost/__snapshots__/index.test.tsx.snap b/src/app/components/TranscriptExperiment/SignPost/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000000..8378351c398
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPost/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,214 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Sign Post should render a 'Help reduce your power consumption' message 1`] = `
+.emotion-0 {
+ position: absolute;
+ color: #FFFFFF;
+ padding: 0.5rem;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-box-flex-wrap: nowrap;
+ -webkit-flex-wrap: nowrap;
+ -ms-flex-wrap: nowrap;
+ flex-wrap: nowrap;
+ background: rgba(14, 98, 0, 0.9);
+ height: 100%;
+ width: 100%;
+ z-index: 1;
+}
+
+@media (min-width: 25rem) {
+ .emotion-0 {
+ left: calc(100% - 15.625rem);
+ width: 15.625rem;
+ height: unset;
+ }
+}
+
+@media (max-width: 18.75rem) {
+ .emotion-0 {
+ padding: 1rem;
+ }
+}
+
+.emotion-1 {
+ fill: #FFFFFF;
+ margin-top: 0.125rem;
+ -webkit-margin-end: 0.5rem;
+ margin-inline-end: 0.5rem;
+ -webkit-margin-start: 0.25rem;
+ margin-inline-start: 0.25rem;
+}
+
+@media screen and (forced-colors: active) {
+ .emotion-1 path {
+ fill: currentColor;
+ }
+}
+
+@media (max-width: 18.75rem) {
+ .emotion-1 {
+ display: none;
+ }
+}
+
+.emotion-2 {
+ color: #141414;
+ font-size: 0.9375rem;
+ line-height: 1.25rem;
+ font-family: ReithSans,Helvetica,Arial,sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ margin: 0;
+ color: #FFFFFF;
+}
+
+@media (min-width: 20rem) and (max-width: 37.4375rem) {
+ .emotion-2 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-2 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+.emotion-3 {
+ margin: 1rem 0 0.25rem 0;
+}
+
+.emotion-4 {
+ all: unset;
+}
+
+.emotion-4:focus {
+ outline: 0.1875rem solid #000000;
+ box-shadow: 0 0 0 0.1875rem #FFFFFF;
+ outline-offset: 0.1875rem;
+}
+
+.emotion-5 {
+ fill: #FFFFFF;
+ margin: 0 0.2rem 0 0;
+}
+
+@media screen and (forced-colors: active) {
+ .emotion-5 path {
+ fill: currentColor;
+ }
+}
+
+.emotion-6 {
+ color: #141414;
+ font-size: 0.9375rem;
+ line-height: 1.25rem;
+ font-family: ReithSans,Helvetica,Arial,sans-serif;
+ font-style: normal;
+ font-weight: 400;
+ margin: 0;
+ color: #FFFFFF;
+ border-bottom: 0.0625rem solid #FFFFFF;
+}
+
+@media (min-width: 20rem) and (max-width: 37.4375rem) {
+ .emotion-6 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+@media (min-width: 37.5rem) {
+ .emotion-6 {
+ font-size: 1rem;
+ line-height: 1.375rem;
+ }
+}
+
+.placeholder:hover .emotion-6,
+.placeholder:focus .emotion-6 {
+ border-bottom: 0.125rem solid #FFFFFF;
+}
+
+.emotion-7 {
+ -webkit-clip-path: inset(100%);
+ clip-path: inset(100%);
+ clip: rect(1px, 1px, 1px, 1px);
+ height: 1px;
+ overflow: hidden;
+ position: absolute;
+ width: 1px;
+ margin: 0;
+}
+
+
+
+
+
+
+
+
+
+
+ Help reduce your power and data usage by not playing video content.
+
+
+
+
+
+
+
+
+
+
+ Load Video
+
+ ,
+
+
+
+
+
+
+
+`;
diff --git a/src/app/components/TranscriptExperiment/SignPost/index.styles.tsx b/src/app/components/TranscriptExperiment/SignPost/index.styles.tsx
new file mode 100644
index 00000000000..8aca6e62ca5
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPost/index.styles.tsx
@@ -0,0 +1,82 @@
+import { focusIndicatorThickness } from '#app/components/ThemeProvider/focusIndicator';
+import pixelsToRem from '#app/utilities/pixelsToRem';
+import { css, Theme } from '@emotion/react';
+
+const WIDTH = 250;
+const BUTTON_COLLAPSE_WIDTH = pixelsToRem(300);
+
+const styles = {
+ container: ({ palette, spacings, mq }: Theme) =>
+ css({
+ position: 'absolute',
+ color: palette.WHITE,
+ padding: `${spacings.FULL}rem`,
+ display: 'flex',
+ flexWrap: 'nowrap',
+ background: `rgba(14, 98, 0, 0.9)`,
+ height: '100%',
+ width: '100%',
+ zIndex: '1',
+ [mq.GROUP_2_MIN_WIDTH]: {
+ left: `calc(100% - ${pixelsToRem(WIDTH)}rem)`,
+ width: `${pixelsToRem(WIDTH)}rem`,
+ height: 'unset',
+ },
+ [`@media (max-width: ${BUTTON_COLLAPSE_WIDTH}rem)`]: {
+ padding: `${spacings.DOUBLE}rem`,
+ },
+ }),
+ icon: ({ mq, palette }: Theme) =>
+ css({
+ fill: palette.WHITE,
+ [mq.FORCED_COLOURS]: {
+ path: {
+ fill: 'currentColor',
+ },
+ },
+ }),
+ fanIcon: ({ spacings }: Theme) =>
+ css({
+ marginTop: `${pixelsToRem(2)}rem`,
+ marginInlineEnd: `${spacings.FULL}rem`,
+ marginInlineStart: `${spacings.HALF}rem`,
+ }),
+ plusIcon: () =>
+ css({
+ margin: `0 0.2rem 0 0`,
+ }),
+ collapsable: () =>
+ css({
+ [`@media (max-width: ${BUTTON_COLLAPSE_WIDTH}rem)`]: {
+ display: 'none',
+ },
+ }),
+ message: ({ palette }: Theme) =>
+ css({
+ margin: '0',
+ color: palette.WHITE,
+ }),
+ underline: ({ palette }: Theme) =>
+ css({
+ borderBottom: `${pixelsToRem(1)}rem solid ${palette.WHITE}`,
+ '.placeholder:hover &, .placeholder:focus &': {
+ borderBottom: `${pixelsToRem(2)}rem solid ${palette.WHITE}`,
+ },
+ }),
+ loadVideoContainer: ({ spacings }: Theme) =>
+ css({
+ margin: `${spacings.DOUBLE}rem 0 ${spacings.HALF}rem 0`,
+ }),
+ loadVideo: ({ palette }: Theme) =>
+ css({
+ all: 'unset',
+ // Izzy
+ '&:focus': {
+ outline: `${focusIndicatorThickness} solid ${palette.BLACK}`,
+ boxShadow: `0 0 0 ${focusIndicatorThickness} ${palette.WHITE}`,
+ outlineOffset: `${focusIndicatorThickness}`,
+ },
+ }),
+};
+
+export default styles;
diff --git a/src/app/components/TranscriptExperiment/SignPost/index.test.tsx b/src/app/components/TranscriptExperiment/SignPost/index.test.tsx
new file mode 100644
index 00000000000..45f325d2ef5
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPost/index.test.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { render } from '../../react-testing-library-with-providers';
+import SignPost from '.';
+
+describe('Sign Post', () => {
+ afterEach(() => {
+ jest.resetModules();
+ });
+
+ it(`should render a 'Help reduce your power consumption' message`, () => {
+ const { container } = render( , {
+ service: 'news',
+ });
+ const text = container.querySelector('p')?.innerHTML;
+
+ expect(text).toEqual(
+ 'Help reduce your power and data usage by not playing video content.',
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/app/components/TranscriptExperiment/SignPost/index.tsx b/src/app/components/TranscriptExperiment/SignPost/index.tsx
new file mode 100644
index 00000000000..7e5a71d3a0c
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPost/index.tsx
@@ -0,0 +1,63 @@
+/** @jsx jsx */
+import { jsx } from '@emotion/react';
+import { useContext, useId } from 'react';
+import { ServiceContext } from '#app/contexts/ServiceContext';
+import Text from '#app/components/Text';
+import VisuallyHiddenText from '#app/components/VisuallyHiddenText';
+import styles from './index.styles';
+import SVGs from './svgs';
+
+const DEFAULT_MESSAGE =
+ 'Help reduce your power and data usage by not playing video content.';
+const DEFAULT_LOAD_TITLE = 'Load Video';
+
+type Props = {
+ title?: string;
+};
+
+const SignPost = ({ title = '' }: Props) => {
+ const {
+ translations: {
+ media: { signPost, loadVideo },
+ },
+ } = useContext(ServiceContext);
+ const idRef = useId();
+
+ const message = signPost ?? DEFAULT_MESSAGE;
+ const buttonLabel = loadVideo ?? DEFAULT_LOAD_TITLE;
+
+ return (
+
+
+
+
+
+ {message}
+
+
+
+
+
+
+
+
+ {buttonLabel}
+ , {title}
+
+
+
+
+
+ );
+};
+
+export default SignPost;
diff --git a/src/app/components/TranscriptExperiment/SignPost/svgs.tsx b/src/app/components/TranscriptExperiment/SignPost/svgs.tsx
new file mode 100644
index 00000000000..cc954ae0927
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPost/svgs.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+const FanSvg = ({ className }: { className?: string }) => (
+
+ {' '}
+
+);
+
+const PlusSvg = ({ className }: { className?: string }) => (
+
+
+
+);
+
+export default { FanSvg, PlusSvg };
diff --git a/src/app/components/TranscriptExperiment/SignPostNoJs/__snapshots__/index.test.tsx.snap b/src/app/components/TranscriptExperiment/SignPostNoJs/__snapshots__/index.test.tsx.snap
new file mode 100644
index 00000000000..8cc3b6dfc59
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPostNoJs/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Sign Post should render a 'Help reduce your power consumption' message 1`] = `
+
+
+
+`;
diff --git a/src/app/components/TranscriptExperiment/SignPostNoJs/index.styles.tsx b/src/app/components/TranscriptExperiment/SignPostNoJs/index.styles.tsx
new file mode 100644
index 00000000000..763b01b8226
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPostNoJs/index.styles.tsx
@@ -0,0 +1,31 @@
+import pixelsToRem from '#app/utilities/pixelsToRem';
+import { css, Theme } from '@emotion/react';
+
+const WIDTH = 250;
+
+const styles = {
+ container: ({ palette, spacings, mq }: Theme) =>
+ css({
+ textAlign: 'start',
+ position: 'absolute',
+ color: palette.WHITE,
+ padding: `${spacings.DOUBLE}rem`,
+ background: `rgba(34, 34, 34, 0.75)`,
+ height: '100%',
+ width: '100%',
+ zIndex: '1',
+ [mq.GROUP_1_MIN_WIDTH]: {
+ textAlign: 'end',
+ left: `calc(100% - ${pixelsToRem(WIDTH)}rem)`,
+ width: `${pixelsToRem(WIDTH)}rem`,
+ height: 'unset',
+ },
+ }),
+ message: ({ palette }: Theme) =>
+ css({
+ color: palette.WHITE,
+ margin: '0',
+ }),
+};
+
+export default styles;
diff --git a/src/app/components/TranscriptExperiment/SignPostNoJs/index.test.tsx b/src/app/components/TranscriptExperiment/SignPostNoJs/index.test.tsx
new file mode 100644
index 00000000000..d590b9e10d6
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPostNoJs/index.test.tsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import { render } from '../../react-testing-library-with-providers';
+import SignPostNoJs from '.';
+
+describe('Sign Post', () => {
+ afterEach(() => {
+ jest.resetModules();
+ });
+
+ it(`should render a 'Help reduce your power consumption' message`, () => {
+ const { container } = render(
+ ,
+ {
+ service: 'news',
+ },
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/src/app/components/TranscriptExperiment/SignPostNoJs/index.tsx b/src/app/components/TranscriptExperiment/SignPostNoJs/index.tsx
new file mode 100644
index 00000000000..2d1abd7f15f
--- /dev/null
+++ b/src/app/components/TranscriptExperiment/SignPostNoJs/index.tsx
@@ -0,0 +1,22 @@
+/** @jsx jsx */
+import { jsx } from '@emotion/react';
+import Text from '#app/components/Text';
+import styles from './index.styles';
+
+type Props = {
+ noJsMessage: string;
+};
+
+const SignPostNoJs = ({ noJsMessage }: Props) => {
+ return (
+
+
+
+ {noJsMessage}
+
+
+
+ );
+};
+
+export default SignPostNoJs;
diff --git a/src/app/components/icons/index.tsx b/src/app/components/icons/index.tsx
index 985c7ed372b..70e94a6c307 100644
--- a/src/app/components/icons/index.tsx
+++ b/src/app/components/icons/index.tsx
@@ -37,96 +37,18 @@ export const RightChevron = ({ className }: { className?: string }) => (
);
-export const Clock = ({ className }: { className?: string }) => (
+export const RightArrow = ({ className }: { className?: string }) => (
-
-
-);
-
-export const Book = ({ className }: { className?: string }) => (
-
-
-
-);
-
-export const Words = ({ className }: { className?: string }) => (
-
-
-
-
-);
-
-export const Articles = ({ className }: { className?: string }) => (
-
-
-
-
-);
-
-export const Calendar = ({ className }: { className?: string }) => (
-
-
-
-);
-
-export const Favourites = ({ className }: { className?: string }) => (
-
-
-
-);
-
-export const Calculator = ({ className }: { className?: string }) => (
-
-
+
+
+
);
diff --git a/src/app/hooks/useExperimentHook/index.test.tsx b/src/app/hooks/useExperimentHook/index.test.tsx
new file mode 100644
index 00000000000..f5d8b47b0a0
--- /dev/null
+++ b/src/app/hooks/useExperimentHook/index.test.tsx
@@ -0,0 +1,104 @@
+import * as OperaMiniHookModule from '#app/hooks/useOperaMiniDetection';
+
+import { Services } from '#app/models/types/global';
+import React, { PropsWithChildren, act } from 'react';
+import { renderHook } from '#app/components/react-testing-library-with-providers';
+import { ServiceContextProvider } from '#app/contexts/ServiceContext';
+import useExperimentHook, { Stages } from '.';
+
+const ServiceContextWrapper =
+ (service: Services) =>
+ ({ children }: PropsWithChildren) => (
+
+ {children}
+
+ );
+
+describe('ExperimentContext', () => {
+ it.each([
+ {
+ title:
+ 'Returns Stage 3 for any service that is not Mundo, provided they are NOT on data saver or low battery',
+ isOperaMini: false,
+ service: 'afaanoromoo' as Services,
+ dataSaver: false,
+ batteryLevel: '1',
+ hasTranscript: true,
+ expected: Stages.STAGE_3,
+ },
+ {
+ title: 'Returns Stage 2 for Mundo under any circumstance',
+ isOperaMini: false,
+ service: 'mundo' as Services,
+ dataSaver: false,
+ batteryLevel: '1',
+ hasTranscript: true,
+ expected: Stages.STAGE_2,
+ },
+ {
+ title: 'Returns Stage 2 for any service with data saver',
+ isOperaMini: false,
+ service: 'afaanoromoo' as Services,
+ dataSaver: true,
+ batteryLevel: '1',
+ hasTranscript: true,
+ expected: Stages.STAGE_2,
+ },
+ {
+ title: 'Returns Stage 2 for any service with power less equal to 20%',
+ isOperaMini: false,
+ service: 'afaanoromoo' as Services,
+ dataSaver: false,
+ batteryLevel: '0.2',
+ hasTranscript: true,
+ expected: Stages.STAGE_2,
+ },
+
+ {
+ title: 'Returns Stage 2 for any service with on operaMini',
+ isOperaMini: true,
+ service: 'afaanoromoo' as Services,
+ dataSaver: false,
+ batteryLevel: '1',
+ hasTranscript: true,
+ expected: Stages.STAGE_2,
+ },
+ {
+ title: 'Returns default stage for Mundo services with no transcript',
+ isOperaMini: true,
+ service: 'mundo' as Services,
+ dataSaver: false,
+ batteryLevel: '1',
+ hasTranscript: false,
+ expected: Stages.DEFAULT,
+ },
+ ])(
+ '$title',
+ async ({
+ isOperaMini,
+ service,
+ dataSaver,
+ batteryLevel,
+ hasTranscript,
+ expected,
+ }) => {
+ global.navigator.connection = { saveData: dataSaver };
+ global.navigator.getBattery = () =>
+ Promise.resolve({ level: batteryLevel });
+
+ jest.spyOn(OperaMiniHookModule, 'default').mockReturnValue(isOperaMini);
+
+ const { current } = await act(async () => {
+ const { result } = await renderHook(
+ () => useExperimentHook(hasTranscript),
+ {
+ wrapper: ServiceContextWrapper(service),
+ },
+ );
+ return result;
+ });
+
+ expect(current).toBe(expected);
+ },
+ );
+});
diff --git a/src/app/hooks/useExperimentHook/index.tsx b/src/app/hooks/useExperimentHook/index.tsx
new file mode 100644
index 00000000000..744e97a3145
--- /dev/null
+++ b/src/app/hooks/useExperimentHook/index.tsx
@@ -0,0 +1,92 @@
+import useOperaMiniDetection from '#app/hooks/useOperaMiniDetection';
+import { Services } from '#app/models/types/global';
+import { useContext, useEffect, useState } from 'react';
+import { ServiceContext } from '../../contexts/ServiceContext';
+
+// Disabled due to bug in ts lint
+// eslint-disable-next-line no-shadow
+export enum Stages {
+ STAGE_1 = 'stage_1',
+ STAGE_2 = 'stage_2',
+ STAGE_3 = 'stage_3',
+ DEFAULT = 'default',
+}
+
+type ExperimentCriteria = Partial<{
+ service: Services;
+ isOperaMini: boolean;
+ dataSaver: boolean;
+ lowPower: boolean;
+ noJs: boolean;
+ hasTranscript: boolean;
+}>;
+
+export type Navigator = {
+ connection: { saveData: boolean };
+ getBattery?: () => Promise<{ level: number }>;
+};
+
+const LOW_POWER_THRESHOLD = 0.2;
+
+const determineStage = ({
+ service,
+ isOperaMini,
+ dataSaver,
+ lowPower,
+ noJs,
+ hasTranscript,
+}: ExperimentCriteria) => {
+ if (noJs) {
+ return Stages.STAGE_1;
+ }
+
+ if (service !== 'mundo' && !lowPower && !dataSaver && !isOperaMini) {
+ return Stages.STAGE_3;
+ }
+
+ if (
+ (service === 'mundo' || dataSaver || isOperaMini || lowPower) &&
+ hasTranscript
+ ) {
+ return Stages.STAGE_2;
+ }
+
+ return Stages.DEFAULT;
+};
+
+const useExperimentHook = (hasTranscript: boolean) => {
+ const [lowPower, setLowPower] = useState(false);
+ const [dataSaver, setSaveDataMode] = useState(false);
+ const [noJs, setNoJs] = useState(true);
+ const isOperaMini = useOperaMiniDetection();
+ const { service } = useContext(ServiceContext);
+
+ useEffect(() => {
+ const initialiseDeviceStates = async () => {
+ const nav = navigator as unknown as Navigator;
+ const saveDataMode = nav.connection?.saveData;
+ if (nav.getBattery) {
+ const manager = await nav.getBattery();
+ const { level } = manager;
+ const isLowPower = level <= LOW_POWER_THRESHOLD;
+ setLowPower(isLowPower);
+ }
+ setSaveDataMode(saveDataMode);
+ };
+ initialiseDeviceStates();
+ setNoJs(false);
+ }, []);
+
+ const stage = determineStage({
+ isOperaMini,
+ service,
+ dataSaver,
+ lowPower,
+ noJs,
+ hasTranscript,
+ });
+
+ return stage;
+};
+
+export default useExperimentHook;
diff --git a/src/app/lib/config/services/mundo.ts b/src/app/lib/config/services/mundo.ts
index 1883747803b..6af55592777 100644
--- a/src/app/lib/config/services/mundo.ts
+++ b/src/app/lib/config/services/mundo.ts
@@ -219,6 +219,9 @@ export const service: DefaultServiceConfig = {
},
},
media: {
+ signPost:
+ 'Ayude a reducir su uso de energía y datos al no reproducir contenido de video.',
+ loadVideo: 'Cargar vídeo',
noJs: 'Para ver este contenido, favor activar JavaScript, o intentar con otro navegador',
contentExpired: 'Este contenido ya no está disponible.',
contentNotYetAvailable: 'Este programa todavía no está disponible.',
diff --git a/src/app/models/types/translations.ts b/src/app/models/types/translations.ts
index 2b47e0071f1..a11d55f7ff5 100644
--- a/src/app/models/types/translations.ts
+++ b/src/app/models/types/translations.ts
@@ -117,6 +117,8 @@ export interface Translations {
};
};
media: {
+ signPost?: string;
+ loadVideo?: string;
noJs?: string;
contentExpired?: string;
contentNotYetAvailable?: string;
diff --git a/src/app/pages/ArticlePage/index.stories.tsx b/src/app/pages/ArticlePage/index.stories.tsx
index ae08a9b07f0..b69ce912cbf 100644
--- a/src/app/pages/ArticlePage/index.stories.tsx
+++ b/src/app/pages/ArticlePage/index.stories.tsx
@@ -13,6 +13,7 @@ import articleDataBurmese from '#data/burmese/articles/cn0exdy1jzvo.json';
import articleDataWithRelatedContent from '#data/afrique/articles/c7yn6nznljdo.json';
import articleDataWithSingleRelatedContent from '#data/afrique/articles/cz216x22106o.json';
import articleDataWithPodcastPromo from '#data/russian/articles/c61q94n3rm3o.json';
+import articleDataWithTranscript from '#data/mundo/articles/cle16n19nd9o.json';
import articleNewsWithPodcastPromo from '#data/news/articles/crkxdvxzwxk2.json';
import articleDataWithElectionTag from '#data/mundo/articles/c206j730722o.json';
import withPageWrapper from '#containers/PageHandlers/withPageWrapper';
@@ -202,6 +203,20 @@ export const ArticlePageWithPodcastNews = () => (
/>
);
+export const ArticlePageWithTranscriptStage2 = () => (
+
+);
+
+export const ArticlePageWithTranscriptStage3 = () => (
+
+);
+
export const ArticlePageWithElectionBanner = {
render: () => (
button,
.emotion-7:focus>button {
@@ -355,6 +370,25 @@ exports[`MediaArticlePage should render a news article correctly 1`] = `
padding-bottom: 0;
}
+.emotion-18:has(+ details) {
+ margin: 0.5rem;
+ width: auto;
+}
+
+@media (min-width: 25rem) and (max-width: 37.4375rem) {
+ .emotion-18:has(+ details) {
+ width: auto;
+ margin: 0.5rem;
+ }
+}
+
+@media (min-width: 63rem) {
+ .emotion-18:has(+ details) {
+ width: auto;
+ margin: 0.5rem;
+ }
+}
+
@media (max-width: 14.9375rem) {
.emotion-20 {
padding: 0 0.5rem;
@@ -805,7 +839,7 @@ exports[`MediaArticlePage should render a news article correctly 1`] = `
data-e2e="media-loader__container"
>
+
button,
.emotion-8:focus>button {
@@ -763,7 +778,7 @@ exports[`OnDemand TV Page Dark Mode Design - should match snapshot 1`] = `
data-e2e="media-loader__container"
>
+
void };
};
}
+
+ interface Navigator {
+ connection: { saveData: boolean };
+ getBattery: () => Promise;
+ }
}
export {};
diff --git a/src/integration/pages/articles/afrique/__snapshots__/canonical.test.js.snap b/src/integration/pages/articles/afrique/__snapshots__/canonical.test.js.snap
index 6f9a4a0740b..03867a8ac5a 100644
--- a/src/integration/pages/articles/afrique/__snapshots__/canonical.test.js.snap
+++ b/src/integration/pages/articles/afrique/__snapshots__/canonical.test.js.snap
@@ -8,19 +8,9 @@ exports[`Canonical Articles Media Loader a11y assistive technology can read the
exports[`Canonical Articles Media Loader renders a figure caption 1`] = `
-
-
- <strong>Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</p></div>
+
-
-
- <strong>Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</p></div>
+
-
-
- <strong>Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Pour regarder ce contenu, veuillez activer JavaScript ou essayer un autre navigateur.</p></div>
+
-
-
- <strong>برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</p></div>
+
-
-
- <strong>برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</p></div>
+
-
-
- <strong>برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">برای پخش این فایل لطفا جاوا اسکریپت را فعال یا از یک مرورگر دیگر استفاده کنید.</p></div>
+
-
-
- <strong>Na'urarku na da matsalar sauraren sauti</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Na'urarku na da matsalar sauraren sauti</p></div>
+
-
-
- <strong>Na'urarku na da matsalar sauraren sauti</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Na'urarku na da matsalar sauraren sauti</p></div>
+
-
-
- <strong>Na'urarku na da matsalar sauraren sauti</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Na'urarku na da matsalar sauraren sauti</p></div>
+
-
-
- <strong>په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</p></div>
+
-
-
- <strong>په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</p></div>
+
-
-
- <strong>په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</strong>
-
-
+
+ <div class="bbc-kvi82l"><p class="bbc-1y1oyo6">په دې وسیله کې د غږ اوريدل او ویډیو لیدنه شونې نه ده.</p></div>
+
- <strong>Na'urarku na da matsalar sauraren sauti</strong>
+
+ <div class="bbc-kvi82l"><p class="bbc-8ugaiq">Na'urarku na da matsalar sauraren sauti</p></div>
`;
diff --git a/ws-nextjs-app/integration/pages/av-embeds/russian/__snapshots__/canonical.test.ts.snap b/ws-nextjs-app/integration/pages/av-embeds/russian/__snapshots__/canonical.test.ts.snap
index b1957a48795..5361b5946c2 100644
--- a/ws-nextjs-app/integration/pages/av-embeds/russian/__snapshots__/canonical.test.ts.snap
+++ b/ws-nextjs-app/integration/pages/av-embeds/russian/__snapshots__/canonical.test.ts.snap
@@ -4,19 +4,9 @@ exports[`Canonical Av-embeds Media Loader a11y assistive technology can read the
exports[`Canonical Av-embeds Media Loader renders a figure caption 1`] = `
-
-
- <strong>Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-eqbegv">Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</p></div>
+
-
-
- <strong>Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-eqbegv">Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</p></div>
+
-
-
- <strong>Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-eqbegv">Для просмотра этого контента вам надо включить JavaScript или использовать другой браузер</p></div>
+
-
-
- Contains strong language and some upsetting scenes.
-
-
- <strong>Dem no support media player for your device</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-8ugaiq">Dem no support media player for your device</p></div>
+
-
-
- Contains strong language and some upsetting scenes.
-
-
- <strong>Dem no support media player for your device</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-8ugaiq">Dem no support media player for your device</p></div>
+
-
-
- Contains strong language and some upsetting scenes.
-
-
- <strong>Dem no support media player for your device</strong>
-
-
+
+ <div class="css-kvi82l"><p class="css-8ugaiq">Dem no support media player for your device</p></div>
+