diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0668a2e5e7d..3ec4a2059e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,12 @@ jobs: -DAPP_SECRET="fake_secret" \ -Duser.timezone=Australia/Sydney \ -jar ./bin/sbt-launch.jar clean compile assets scalafmtCheckAll test Universal/packageBin + + - name: Test Summary + uses: test-summary/action@v2 + with: + paths: "test-results/**/TEST-*.xml" + if: always() - uses: guardian/actions-riff-raff@v4 env: diff --git a/.gitignore b/.gitignore index 6fbf8f6de5c..725f9848c0d 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ static/src/stylesheets/pasteup/.npmrc metals.sbt .java-version +test-results/ diff --git a/applications/app/controllers/CrosswordsController.scala b/applications/app/controllers/CrosswordsController.scala index 48c16256bec..dbbe78e15f3 100644 --- a/applications/app/controllers/CrosswordsController.scala +++ b/applications/app/controllers/CrosswordsController.scala @@ -1,10 +1,16 @@ package controllers -import com.gu.contentapi.client.model.v1.CrosswordType.{Cryptic, Quick} -import com.gu.contentapi.client.model.v1.{Crossword, ItemResponse, Content => ApiContent, Section => ApiSection} +import com.gu.contentapi.client.model.v1.{ + Crossword, + ItemResponse, + SearchResponse, + Content => ApiContent, + Section => ApiSection, +} import common.{Edition, GuLogging, ImplicitControllerExecutionContext} import conf.Static import contentapi.ContentApiClient +import com.gu.contentapi.client.model.SearchQuery import crosswords.{ AccessibleCrosswordPage, AccessibleCrosswordRows, @@ -18,6 +24,7 @@ import html.HtmlPageHelpers.ContentCSSFile import model.Cached.{RevalidatableResult, WithoutRevalidationResult} import model._ import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel +import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel.toJson import model.dotcomrendering.{DotcomRenderingDataModel, PageType} import org.joda.time.{DateTime, LocalDate} import pages.{CrosswordHtmlPage, IndexHtmlPage, PrintableCrosswordHtmlPage} @@ -304,21 +311,44 @@ class CrosswordEditionsController( def digitalEdition: Action[AnyContent] = Action.async { implicit request => getCrosswords .map(parseCrosswords) - .flatMap { - case Some(crosswordPage) => - remoteRenderer.getEditionsCrossword(wsClient, crosswordPage) - case None => Future.successful(NotFound) + .flatMap { crosswords => + remoteRenderer.getEditionsCrossword(wsClient, crosswords) } } - private lazy val crosswordsQuery = contentApiClient.item("crosswords") + def digitalEditionJson: Action[AnyContent] = Action.async { implicit request => + getCrosswords + .map(parseCrosswords) + .map { crosswords => + Cached(CacheTime.Default)(RevalidatableResult.Ok(toJson(crosswords))).as("application/json") + } + } - private def getCrosswords: Future[ItemResponse] = contentApiClient.getResponse(crosswordsQuery) + private def getCrosswords: Future[SearchResponse] = + contentApiClient.getResponse(crosswordsQuery) + + /** Search for playable crosswords sorted by print publication date. This will exclude older, originally print-only + * crosswords that happen to have been re-published in a digital format recently. + */ + private lazy val crosswordsQuery = + SearchQuery() + .contentType("crossword") + .tag(crosswordTags) + .useDate("newspaper-edition") + .pageSize(25) + + private lazy val crosswordTags = Seq( + "crosswords/series/quick", + "crosswords/series/cryptic", + "crosswords/series/prize", + "crosswords/series/weekend-crossword", + "crosswords/series/quick-cryptic", + "crosswords/series/everyman", + "crosswords/series/speedy", + "crosswords/series/quiptic", + ).mkString("|") + + private def parseCrosswords(response: SearchResponse): EditionsCrosswordRenderingDataModel = + EditionsCrosswordRenderingDataModel(response.results.flatMap(_.crossword)) - private def parseCrosswords(response: ItemResponse): Option[EditionsCrosswordRenderingDataModel] = - for { - results <- response.results - quick <- results.find(_.crossword.exists(_.`type` == Quick)).flatMap(_.crossword) - cryptic <- results.find(_.crossword.exists(_.`type` == Cryptic)).flatMap(_.crossword) - } yield EditionsCrosswordRenderingDataModel(quick, cryptic) } diff --git a/applications/conf/routes b/applications/conf/routes index e17e9272bd6..2cc50e84c62 100644 --- a/applications/conf/routes +++ b/applications/conf/routes @@ -29,6 +29,7 @@ GET /crosswords/lookup # Crosswords digital edition GET /crosswords/digital-edition controllers.CrosswordEditionsController.digitalEdition +GET /crosswords/digital-edition.json controllers.CrosswordEditionsController.digitalEditionJson # Email paths GET /email/form/$emailType/$listId<[0-9]+> controllers.EmailSignupController.renderForm(emailType: String, listId: Int) diff --git a/common/app/conf/switches/ABTestSwitches.scala b/common/app/conf/switches/ABTestSwitches.scala index 3a1af373203..3f1c921eb30 100644 --- a/common/app/conf/switches/ABTestSwitches.scala +++ b/common/app/conf/switches/ABTestSwitches.scala @@ -44,7 +44,7 @@ trait ABTestSwitches { "Test the Opt Out frequency capping feature", owners = Seq(Owner.withEmail("commercial.dev@theguardian.com")), safeState = Off, - sellByDate = Some(LocalDate.of(2024, 12, 2)), + sellByDate = Some(LocalDate.of(2025, 1, 29)), exposeClientSide = true, highImpact = false, ) @@ -71,16 +71,6 @@ trait ABTestSwitches { highImpact = false, ) - Switch( - ABTests, - "ab-new-header-bidding-endpoint", - "Test new header bidding (prebid) analytics endpoint", - owners = Seq(Owner.withEmail("commercial.dev@theguardian.com")), - safeState = Off, - sellByDate = Some(LocalDate.of(2024, 12, 2)), - exposeClientSide = true, - highImpact = false, - ) Switch( ABTests, "ab-gpid-prebid-ad-units", diff --git a/common/app/conf/switches/FeatureSwitches.scala b/common/app/conf/switches/FeatureSwitches.scala index 99cd0c0565f..7b3a2d5c49c 100644 --- a/common/app/conf/switches/FeatureSwitches.scala +++ b/common/app/conf/switches/FeatureSwitches.scala @@ -567,9 +567,9 @@ trait FeatureSwitches { group = SwitchGroup.Feature, name = "disable-front-container-show-hide", description = "For users with no currently hidden containers on a front, removes the ability to hide containers", - owners = Seq(Owner.withGithub("cemms1")), + owners = Seq(Owner.withEmail("project.fairground@theguardian.com")), safeState = On, - sellByDate = LocalDate.of(2024, 11, 29), + sellByDate = LocalDate.of(2025, 2, 4), exposeClientSide = true, highImpact = false, ) diff --git a/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala b/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala index 92cfff9ec8a..180e91803ac 100644 --- a/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala +++ b/common/app/model/dotcomrendering/pageElements/EditionsCrosswordRenderingDataModel.scala @@ -4,17 +4,30 @@ import com.gu.contentapi.client.model.v1.Crossword import com.gu.contentapi.json.CirceEncoders._ import io.circe.JsonObject import io.circe.syntax._ +import implicits.Dates.CapiRichDateTime case class EditionsCrosswordRenderingDataModel( - quick: Crossword, - cryptic: Crossword, + crosswords: Iterable[Crossword], ) object EditionsCrosswordRenderingDataModel { + def apply(crosswords: Iterable[Crossword]): EditionsCrosswordRenderingDataModel = + new EditionsCrosswordRenderingDataModel(crosswords.map(crossword => { + val shipSolutions = + crossword.dateSolutionAvailable + .map(_.toJoda.isBeforeNow) + .getOrElse(crossword.solutionAvailable) + + if (shipSolutions) { + crossword + } else { + crossword.copy(entries = crossword.entries.map(_.copy(solution = None))) + } + })) + def toJson(model: EditionsCrosswordRenderingDataModel): String = { JsonObject( - "quick" -> model.quick.asJson.dropNullValues, - "cryptic" -> model.cryptic.asJson.dropNullValues, - ).asJson.dropNullValues.noSpaces + "crosswords" -> model.crosswords.asJson.deepDropNullValues, + ).asJson.noSpaces } } diff --git a/common/app/navigation/NavLinks.scala b/common/app/navigation/NavLinks.scala index 37473d158f8..f5bc9fc3215 100644 --- a/common/app/navigation/NavLinks.scala +++ b/common/app/navigation/NavLinks.scala @@ -319,11 +319,12 @@ object NavLinks { usNews, usPolitics, world, - usEnvironment, + climateCrisis, middleEast, ukraine, usSoccer, usBusiness, + usEnvironment, usTech, science, newsletters, diff --git a/common/test/helpers/FaciaTestData.scala b/common/test/helpers/FaciaTestData.scala index cc0ee54002e..8229003a2e5 100644 --- a/common/test/helpers/FaciaTestData.scala +++ b/common/test/helpers/FaciaTestData.scala @@ -70,6 +70,22 @@ trait FaciaTestData extends ModelHelper { "/commentisfree/2013/oct/07/feminism-rebranding-man-hater", ) + val europeFrontTrailIds: Seq[String] = Seq( + "/world/2024/nov/27/plan-to-cut-berlin-arts-budget-will-destroy-citys-culture-directors-warn", + "/stage/2024/nov/27/afd-threats-to-german-democracy-on-stage-maximilian-steinbeis-a-citizen-of-the-people", + "/commentisfree/2024/nov/27/france-cannabis-consumption-europe-tax-decriminalisation-crime", + "/football/2024/nov/27/bodo-glimt-manchester-united-europa-league", + "/books/2024/nov/26/freedom-by-angela-merkel-review-settling-scores-with-silence", + ) + + val europeBetaFrontTrailIds: Seq[String] = Seq( + "/business/2024/nov/27/just-eat-to-delist-from-london-stock-exchange-to-cut-complexity-and-costs", + "/world/2024/nov/26/uk-labour-cabinet-ministers-sanctions-russia-storm-shadow-missiles", + "/business/2024/nov/27/fining-budget-airlines-will-make-flying-more-expensive-says-easyjet-boss", + "/world/2024/nov/26/irish-pm-simon-harris-slump-drops-points-polls-election-fine-gael", + "/commentisfree/2024/nov/25/the-guardian-view-on-romanias-presidential-election-a-stable-ukrainian-ally-wobblese", + ) + val cultureTrailIds: Seq[String] = Seq( "/film/2013/oct/08/gravity-science-astrophysicist", @@ -82,6 +98,8 @@ trait FaciaTestData extends ModelHelper { val ukFrontTrails: Seq[PressedContent] = ukFrontTrailIds map TestContent.newFaciaContent val usFrontTrails: Seq[PressedContent] = usFrontTrailIds map TestContent.newFaciaContent val auFrontTrails: Seq[PressedContent] = auFrontTrailIds map TestContent.newFaciaContent + val europeFrontTrails: Seq[PressedContent] = europeFrontTrailIds map TestContent.newFaciaContent + val europeBetaFrontTrails: Seq[PressedContent] = europeBetaFrontTrailIds map TestContent.newFaciaContent val cultureFrontTrails: Seq[PressedContent] = cultureTrailIds map TestContent.newFaciaContent @@ -172,6 +190,64 @@ trait FaciaTestData extends ModelHelper { ), ) + val europeFaciaPage: PressedPage = PressedPage( + id = "europe", + SeoData.fromPath("europe"), + FrontProperties.empty, + collections = List( + PressedCollection( + id = "europe/news/regular-stories", + displayName = "", + curated = europeFrontTrails.toList, + backfill = Nil, + treats = Nil, + lastUpdated = None, + href = None, + description = None, + collectionType = "", + groups = None, + uneditable = false, + showTags = false, + showSections = false, + hideKickers = false, + showDateHeader = false, + showLatestUpdate = false, + config = CollectionConfig.empty, + hasMore = false, + targetedTerritory = None, + ), + ), + ) + + val europeBetaFaciaPageWithTargetedTerritory: PressedPage = PressedPage( + id = "europe-beta", + SeoData.fromPath("europe-beta"), + FrontProperties.empty, + collections = List( + PressedCollection( + id = "europe-beta/news/regular-stories", + displayName = "", + curated = europeBetaFrontTrails.toList, + backfill = Nil, + treats = Nil, + lastUpdated = None, + href = None, + description = None, + collectionType = "", + groups = None, + uneditable = false, + showTags = false, + showSections = false, + hideKickers = false, + showDateHeader = false, + showLatestUpdate = false, + config = CollectionConfig.empty, + hasMore = false, + targetedTerritory = Some(EU27Territory), + ), + ), + ) + val ukCultureFaciaPage: PressedPage = PressedPage( id = "uk/culture", SeoData.fromPath("uk/culture"), @@ -334,6 +410,8 @@ trait FaciaTestData extends ModelHelper { ("uk", new TestPageFront("uk", Uk, ukFaciaPage)), ("us", new TestPageFront("us", Us, usFaciaPage)), ("au", new TestPageFront("au", Au, auFaciaPage)), + ("europe", new TestPageFront("europe", Au, europeFaciaPage)), + ("europe-beta", new TestPageFront("europe-beta", Au, europeBetaFaciaPageWithTargetedTerritory)), ("uk/culture", new TestPageFront("uk/culture", Uk, ukCultureFaciaPage)), ("us/culture", new TestPageFront("us/culture", Us, usCultureFaciaPage)), ("au/culture", new TestPageFront("au/culture", Au, auCultureFaciaPage)), diff --git a/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala b/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala new file mode 100644 index 00000000000..5dc1d09d898 --- /dev/null +++ b/common/test/model/dotcomrendering/EditionsCrosswordRenderingDataModelTest.scala @@ -0,0 +1,88 @@ +package model.dotcomrendering + +import com.gu.contentapi.client.model.v1.{CapiDateTime, Crossword, CrosswordType, CrosswordDimensions, CrosswordEntry} +import model.dotcomrendering.pageElements.EditionsCrosswordRenderingDataModel +import org.mockito.Mockito.when +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import org.scalatestplus.mockito.MockitoSugar +import org.joda.time.DateTime + +class EditionsCrosswordRenderingDataModelTest extends AnyFlatSpec with Matchers with MockitoSugar { + val mockEntry = CrosswordEntry( + id = "mockId", + solution = Some("Mock solution"), + ) + + val mockCrossword = Crossword( + name = "Mock name", + `type` = CrosswordType.Quick, + number = 1, + date = CapiDateTime(DateTime.now().getMillis(), "date"), + dimensions = CrosswordDimensions(1, 1), + entries = Seq(mockEntry, mockEntry), + solutionAvailable = true, + hasNumbers = false, + randomCluesOrdering = false, + ) + + "apply" should "provide solutions when 'dateSolutionAvailable' is in the past" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = Some(CapiDateTime(DateTime.now().minusDays(1).getMillis(), "date")), + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)).crosswords.toSeq + + crosswords(0).entries(0).solution shouldBe Some("Mock solution") + crosswords(0).entries(1).solution shouldBe Some("Mock solution") + crosswords(1).entries(0).solution shouldBe Some("Mock solution") + crosswords(1).entries(1).solution shouldBe Some("Mock solution") + } + + "apply" should "provide solutions when 'dateSolutionAvailable' is 'None' and solutionAvailable is 'true'" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = None, + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)).crosswords.toSeq + + crosswords(0).entries(0).solution shouldBe Some("Mock solution") + crosswords(0).entries(1).solution shouldBe Some("Mock solution") + crosswords(1).entries(0).solution shouldBe Some("Mock solution") + crosswords(1).entries(1).solution shouldBe Some("Mock solution") + } + + "apply" should "not provide solutions when 'dateSolutionAvailable' is in the future" in { + val crossword = mockCrossword.copy( + solutionAvailable = true, + dateSolutionAvailable = Some(CapiDateTime(DateTime.now().plusDays(1).getMillis(), "date")), + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)).crosswords.toSeq + + crosswords(0).entries(0).solution shouldBe None + crosswords(0).entries(1).solution shouldBe None + crosswords(1).entries(0).solution shouldBe None + crosswords(1).entries(1).solution shouldBe None + } + + "apply" should "not provide solutions when 'dateSolutionAvailable' is 'None' and solutionAvailable is 'false'" in { + val crossword = mockCrossword.copy( + solutionAvailable = false, + dateSolutionAvailable = None, + ) + + val crosswords = + EditionsCrosswordRenderingDataModel(Seq(crossword, crossword)).crosswords.toSeq + + crosswords(0).entries(0).solution shouldBe None + crosswords(0).entries(1).solution shouldBe None + crosswords(1).entries(0).solution shouldBe None + crosswords(1).entries(1).solution shouldBe None + } +} diff --git a/common/test/resources/reference-navigation.json b/common/test/resources/reference-navigation.json index c01ea6a3611..ac236950a81 100644 --- a/common/test/resources/reference-navigation.json +++ b/common/test/resources/reference-navigation.json @@ -1070,40 +1070,9 @@ "classList": [] }, { - "title": "Environment", - "url": "/us/environment", - "children": [ - { - "title": "Climate crisis", - "url": "/environment/climate-crisis", - "children": [], - "classList": [] - }, - { - "title": "Wildlife", - "url": "/environment/wildlife", - "children": [], - "classList": [] - }, - { - "title": "Energy", - "url": "/environment/energy", - "children": [], - "classList": [] - }, - { - "title": "Pollution", - "url": "/environment/pollution", - "children": [], - "classList": [] - }, - { - "title": "Green light", - "url": "/environment/series/green-light", - "children": [], - "classList": [] - } - ], + "title": "Climate crisis", + "url": "/environment/climate-crisis", + "children": [], "classList": [] }, { @@ -1198,6 +1167,43 @@ ], "classList": [] }, + { + "title": "Environment", + "url": "/us/environment", + "children": [ + { + "title": "Climate crisis", + "url": "/environment/climate-crisis", + "children": [], + "classList": [] + }, + { + "title": "Wildlife", + "url": "/environment/wildlife", + "children": [], + "classList": [] + }, + { + "title": "Energy", + "url": "/environment/energy", + "children": [], + "classList": [] + }, + { + "title": "Pollution", + "url": "/environment/pollution", + "children": [], + "classList": [] + }, + { + "title": "Green light", + "url": "/environment/series/green-light", + "children": [], + "classList": [] + } + ], + "classList": [] + }, { "title": "Tech", "url": "/us/technology", diff --git a/dev-build/conf/routes b/dev-build/conf/routes index a913457e946..8329b037133 100644 --- a/dev-build/conf/routes +++ b/dev-build/conf/routes @@ -31,6 +31,7 @@ GET /crosswords/lookup # Crosswords digital edition GET /crosswords/digital-edition controllers.CrosswordEditionsController.digitalEdition +GET /crosswords/digital-edition.json controllers.CrosswordEditionsController.digitalEditionJson # Email paths GET /email/form/$emailType/$listId<[0-9]+> controllers.EmailSignupController.renderForm(emailType: String, listId: Int) diff --git a/facia/app/controllers/FaciaController.scala b/facia/app/controllers/FaciaController.scala index 3b9277a4fd0..4ffb55d8632 100644 --- a/facia/app/controllers/FaciaController.scala +++ b/facia/app/controllers/FaciaController.scala @@ -151,9 +151,9 @@ trait FaciaController Action.async { implicit request => if (shouldEditionRedirect(path)) redirectTo(Editionalise(path, Edition(request))) - else if (!ConfigAgent.shouldServeFront(path) || request.getQueryString("page").isDefined) + else if (!ConfigAgent.shouldServeFront(path) || request.getQueryString("page").isDefined) { applicationsRedirect(path) - else + } else renderFrontPressResult(path) } @@ -219,40 +219,28 @@ trait FaciaController targetedTerritories, ) - import PressedPage.pressedPageFormat private[controllers] def renderFrontPressResult(path: String)(implicit request: RequestHeader): Future[Result] = { - // Europe beta experiment - // Phase 1 prevents users from being able to view the europe-beta front unless opted into the test - val inEuropeBetaTest = ActiveExperiments.isParticipating(EuropeBetaFront) - if (path == "europe-beta" && !inEuropeBetaTest && !context.isPreview) { - return successful(Cached(CacheTime.NotFound)(WithoutRevalidationResult(NotFound))) - } - - val futureFaciaPage: Future[Option[(PressedPage, Boolean)]] = frontJsonFapi.get(path, liteRequestType).flatMap { - case Some(faciaPage: PressedPage) => - val pageContainsTargetedCollections = TargetedCollections.pageContainsTargetedCollections(faciaPage) - val regionalFaciaPage = TargetedCollections.processTargetedCollections( - faciaPage, - request.territories, - context.isPreview, - pageContainsTargetedCollections, - ) - if (conf.Configuration.environment.stage == "CODE") { - logInfoWithCustomFields( - s"Rendering front $path, frontjson: ${Json.stringify(Json.toJson(faciaPage)(pressedPageFormat))}", - List(), - ) - } - if (faciaPage.collections.isEmpty && liteRequestType == LiteAdFreeType) { - frontJsonFapi.get(path, LiteType).map(_.map(f => (f, false))) - } else Future.successful(Some(regionalFaciaPage, pageContainsTargetedCollections)) - case None => Future.successful(None) + val futureFaciaPage = getFaciaPage(path) + + /** Europe Beta test: swaps the collections on the Europe network front with those on the hidden europe-beta front + * for users participating in the test + */ + val futureFaciaPageWithEuropeBetaTest: Future[Option[(PressedPage, Boolean)]] = { + if (path == "europe" && ActiveExperiments.isParticipating(EuropeBetaFront)) { + val futureEuropeBetaPage = getFaciaPage("europe-beta") + for { + europePage <- futureFaciaPage + europeBetaPage <- futureEuropeBetaPage + } yield replaceFaciaPageCollections(europePage, europeBetaPage) + } else { + futureFaciaPage + } } val networkFrontEdition = Edition.allEditions.find(_.networkFrontId == path) val deeplyRead = networkFrontEdition.map(deeplyReadAgent.getTrails) - val futureResult = futureFaciaPage.flatMap { + val futureResult = futureFaciaPageWithEuropeBetaTest.flatMap { case Some((faciaPage, _)) if nonHtmlEmail(request) => successful(Cached(CacheTime.RecentlyUpdated)(renderEmail(faciaPage))) case Some((faciaPage: PressedPage, targetedTerritories)) @@ -316,6 +304,53 @@ trait FaciaController futureResult } + import PressedPage.pressedPageFormat + + /** Fetches facia page for path */ + private[controllers] def getFaciaPage(path: String)(implicit + request: RequestHeader, + ): Future[Option[(PressedPage, Boolean)]] = frontJsonFapi.get(path, liteRequestType).flatMap { + case Some(faciaPage: PressedPage) if faciaPage.collections.isEmpty && liteRequestType == LiteAdFreeType => + frontJsonFapi.get(path, LiteType).map(_.map(f => (f, false))) + case Some(faciaPage: PressedPage) => + val pageContainsTargetedCollections = TargetedCollections.pageContainsTargetedCollections(faciaPage) + val regionalFaciaPage = TargetedCollections.processTargetedCollections( + faciaPage, + request.territories, + context.isPreview, + pageContainsTargetedCollections, + ) + if (conf.Configuration.environment.stage == "CODE") { + logInfoWithCustomFields( + s"Rendering front $path, frontjson: ${Json.stringify(Json.toJson(faciaPage)(pressedPageFormat))}", + List(), + ) + } + Future.successful(Some(regionalFaciaPage, pageContainsTargetedCollections)) + case None => Future.successful(None) + } + + /** Swaps collections on a given facia page with those on another facia page. Set up for the Europe beta test where we + * return europe-beta collections on the europe front if participating in the test + */ + private[controllers] def replaceFaciaPageCollections( + baseFaciaPage: Option[(PressedPage, Boolean)], + replacementFaciaPage: Option[(PressedPage, Boolean)], + ): Option[(PressedPage, Boolean)] = { + for { + (basePage, _) <- baseFaciaPage + (replacementPage, replacementTargetedTerritories) <- replacementFaciaPage + } yield ( + PressedPage( + id = basePage.id, + seoData = basePage.seoData, + frontProperties = basePage.frontProperties, + collections = replacementPage.collections, + ), + replacementTargetedTerritories, + ) + } + private def renderEmail(faciaPage: PressedPage)(implicit request: RequestHeader) = { if (request.isHeadlineText) { FrontHeadline.renderEmailHeadline(faciaPage) diff --git a/facia/test/FaciaControllerTest.scala b/facia/test/FaciaControllerTest.scala index 0af2e567ad5..b69f76367b3 100644 --- a/facia/test/FaciaControllerTest.scala +++ b/facia/test/FaciaControllerTest.scala @@ -4,9 +4,12 @@ import agents.{DeeplyReadAgent, MostViewedAgent} import com.fasterxml.jackson.core.JsonParseException import com.gu.facia.client.models.{ConfigJson, FrontJson} import common.editions.{Uk, Us} +import common.facia.FixtureBuilder import controllers.{Assets, FaciaControllerImpl} +import experiments.{ActiveExperiments, EuropeBetaFront, ParticipationGroups} import helpers.FaciaTestData import implicits.FakeRequests +import model.{FrontProperties, PressedPage, SeoData} import org.mockito.Matchers.{any, anyString} import org.mockito.Mockito.when import org.scalatest._ @@ -35,7 +38,8 @@ import scala.concurrent.{Await, Future} with MockitoSugar with WithTestFrontJsonFapi with WithTestContentApiClient - with WithAssets { + with WithAssets + with PrivateMethodTester { lazy val wsClient = mockWsResponse() @@ -87,6 +91,7 @@ import scala.concurrent.{Await, Future} "uk" -> frontJson, "au/media" -> frontJson, "email/uk/daily" -> frontJson, + "europe" -> frontJson, ), collections = Map.empty, ), @@ -257,4 +262,27 @@ import scala.concurrent.{Await, Future} status(result) should be(200) header("X-GU-Dotcomponents", result) should be(None) } + + "FaciaController.replaceFaciaPageCollections" should "replace the collections of a pressed page with those on another pressed page" in { + val europePage: Option[(PressedPage, Boolean)] = Some( + europeFaciaPage, + false, + ) + val europeBetaPage: Option[(PressedPage, Boolean)] = Some( + europeBetaFaciaPageWithTargetedTerritory, + true, + ) + val replaceFaciaPageCollections = + PrivateMethod[Option[(PressedPage, Boolean)]](Symbol("replaceFaciaPageCollections")) + val result = faciaController invokePrivate replaceFaciaPageCollections(europePage, europeBetaPage) + val (resultPressedPage, targetedTerritories) = result.get + // The page metadata should remain unchanged + resultPressedPage.id should be("europe") + resultPressedPage.id should not be "europe-beta" + // The collections should come from the replacement page not the original page + resultPressedPage.collections.exists(_ == europeBetaFaciaPageWithTargetedTerritory.collections(0)) should be(true) + resultPressedPage.collections.exists(_ == europeFaciaPage.collections(0)) should be(false) + // The value for targetedTerritories should come from the page with replacement collections + targetedTerritories should be(true) + } } diff --git a/project/ProjectSettings.scala b/project/ProjectSettings.scala index c1f41d0c79b..a57bf3c5c68 100644 --- a/project/ProjectSettings.scala +++ b/project/ProjectSettings.scala @@ -55,8 +55,7 @@ object ProjectSettings { def testStage = if (isCi) "DEVINFRA" else "LOCALTEST" val frontendTestSettings = Seq( - // Use ScalaTest https://groups.google.com/d/topic/play-framework/rZBfNoGtC0M/discussion - Test / testOptions := Nil, + Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-u", s"test-results/scala-${scalaVersion.value}", "-o"), concurrentRestrictions in Global := List(Tags.limit(Tags.Test, 4)), // Copy unit test resources https://groups.google.com/d/topic/play-framework/XD3X6R-s5Mc/discussion Test / unmanagedClasspath += (baseDirectory map { bd => Attributed.blank(bd / "test") }).value, diff --git a/static/src/stylesheets/module/email-signup/_form.scss b/static/src/stylesheets/module/email-signup/_form.scss index df7b463f32a..da0c9c7da75 100644 --- a/static/src/stylesheets/module/email-signup/_form.scss +++ b/static/src/stylesheets/module/email-signup/_form.scss @@ -316,6 +316,18 @@ } } +.email-sub__form--thrasher-the-filter { + .email-sub__thrasher-embed-label { + color: #121212; + } + + .email-sub__thrasher-embed-button { + background-color: $news-dark; + color: $brightness-100; + fill: $brightness-100; + } +} + .email-sub__thrasher-embed-icon { display: inline-flex; height: 100%;