From cf5963cff2340ed7fb79873aff7e22d03d385b95 Mon Sep 17 00:00:00 2001 From: Emma Imber Date: Mon, 23 Mar 2026 17:00:38 +0000 Subject: [PATCH 1/4] Add new variant to test and update logic accordingly --- ab-testing/config/abTests.ts | 2 +- .../components/ProductSummary.Importable.tsx | 14 +++++++++ .../components/StackedProducts.importable.tsx | 10 +++++-- .../components/StackedProducts.stories.tsx | 2 ++ .../enhance-product-summary.test-helpers.ts | 14 +++++++-- .../src/model/enhance-product-summary.test.ts | 30 ++++++++++++------- .../src/model/enhance-product-summary.ts | 8 +++-- dotcom-rendering/src/types/content.ts | 2 +- 8 files changed, 62 insertions(+), 20 deletions(-) diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts index 8b67aa52a94..50d5eb4bf66 100644 --- a/ab-testing/config/abTests.ts +++ b/ab-testing/config/abTests.ts @@ -78,7 +78,7 @@ const ABTests: ABTest[] = [ status: "ON", audienceSize: 0 / 100, audienceSpace: "C", - groups: ["control", "stacked", "carousel"], + groups: ["control", "carousel", "stacked-default", "stacked-expanded"], shouldForceMetricsCollection: false, }, { diff --git a/dotcom-rendering/src/components/ProductSummary.Importable.tsx b/dotcom-rendering/src/components/ProductSummary.Importable.tsx index 8596b4ac1c8..bc0b5fba68d 100644 --- a/dotcom-rendering/src/components/ProductSummary.Importable.tsx +++ b/dotcom-rendering/src/components/ProductSummary.Importable.tsx @@ -22,12 +22,26 @@ export const ProductSummary = ({ ); } + if (variant === 'stacked-default') { + return ( + + + + ); + } + return ( ); diff --git a/dotcom-rendering/src/components/StackedProducts.importable.tsx b/dotcom-rendering/src/components/StackedProducts.importable.tsx index c41cbc6d60f..7d491f1020e 100644 --- a/dotcom-rendering/src/components/StackedProducts.importable.tsx +++ b/dotcom-rendering/src/components/StackedProducts.importable.tsx @@ -33,10 +33,12 @@ export const StackedProducts = ({ products, heading, format, + showAllProducts, }: { products: ProductBlockElement[]; heading: string; format: ArticleFormat; + showAllProducts: boolean; }) => { const [isExpanded, setIsExpanded] = useState(false); return ( @@ -54,7 +56,7 @@ export const StackedProducts = ({ {heading} - {products.length > cardsShownByDefault && ( + {products.length > cardsShownByDefault && !showAllProducts && (

{isExpanded ? products.length @@ -82,7 +84,9 @@ export const StackedProducts = ({ data-component={`at-a-glance-stacked-card-${index + 1}`} style={{ display: - !isExpanded && index >= cardsShownByDefault + !isExpanded && + index >= cardsShownByDefault && + !showAllProducts ? 'none' : 'block', }} @@ -95,7 +99,7 @@ export const StackedProducts = ({ ))} - {products.length > cardsShownByDefault && ( + {products.length > cardsShownByDefault && !showAllProducts && ( setIsExpanded(!isExpanded)} cssOverrides={showAllButtonStyles} diff --git a/dotcom-rendering/src/components/StackedProducts.stories.tsx b/dotcom-rendering/src/components/StackedProducts.stories.tsx index 617cffe5fee..df0c78d1785 100644 --- a/dotcom-rendering/src/components/StackedProducts.stories.tsx +++ b/dotcom-rendering/src/components/StackedProducts.stories.tsx @@ -15,6 +15,7 @@ const meta = { display: ArticleDisplay.Standard, theme: Pillar.Lifestyle, }, + showAllProducts: false, }, decorators: [centreColumnDecorator], } satisfies Meta; @@ -34,5 +35,6 @@ export const FourProducts = { display: ArticleDisplay.Standard, theme: Pillar.Lifestyle, }, + showAllProducts: false, }, } satisfies Story; diff --git a/dotcom-rendering/src/model/enhance-product-summary.test-helpers.ts b/dotcom-rendering/src/model/enhance-product-summary.test-helpers.ts index c17b60e49ad..e849eba292b 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.test-helpers.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.test-helpers.ts @@ -48,12 +48,22 @@ export const findCarousel = ( el.variant === 'carousel', ); -export const findStacked = ( +export const findStackedDefault = ( elements: FEElement[], ): ProductSummaryElement | undefined => elements.find( (el): el is ProductSummaryElement => el._type === 'model.dotcomrendering.pageElements.ProductSummaryElement' && - el.variant === 'stacked', + el.variant === 'stacked-default', + ); + +export const findStackedExpanded = ( + elements: FEElement[], +): ProductSummaryElement | undefined => + elements.find( + (el): el is ProductSummaryElement => + el._type === + 'model.dotcomrendering.pageElements.ProductSummaryElement' && + el.variant === 'stacked-expanded', ); diff --git a/dotcom-rendering/src/model/enhance-product-summary.test.ts b/dotcom-rendering/src/model/enhance-product-summary.test.ts index fd6bcc4be68..48e6e5be162 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.test.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.test.ts @@ -3,7 +3,7 @@ import { atAGlanceHeading, dividerElement, findCarousel, - findStacked, + findStackedDefault, linkElement, productElement, textElement, @@ -222,7 +222,9 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, - serverSideABTests: { 'thefilter-at-a-glance-redesign': 'carousel' }, + serverSideABTests: { + 'thefilter-at-a-glance-redesign': 'carousel', + }, renderingTarget: 'Web', filterAtAGlanceEnabled: true, })(input); @@ -264,14 +266,16 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, - serverSideABTests: { 'thefilter-at-a-glance-redesign': 'stacked' }, + serverSideABTests: { + 'thefilter-at-a-glance-redesign': 'stacked-default', + }, renderingTarget: 'Web', filterAtAGlanceEnabled: true, })(input); - const stacked = findStacked(output); + const stackedDefault = findStackedDefault(output); - expect(stacked).toBeDefined(); + expect(stackedDefault).toBeDefined(); }); it('does not return stacked cards when the rendering target is apps', () => { @@ -306,14 +310,16 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, - serverSideABTests: { 'thefilter-at-a-glance-redesign': 'stacked' }, + serverSideABTests: { + 'thefilter-at-a-glance-redesign': 'stacked-default', + }, renderingTarget: 'Apps', filterAtAGlanceEnabled: true, })(input); - const stacked = findStacked(output); + const stackedDefault = findStackedDefault(output); - expect(stacked).toBeUndefined(); + expect(stackedDefault).toBeUndefined(); }); it('does not return stacked cards when the filterAtAGlance switch is OFF', () => { @@ -348,13 +354,15 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, - serverSideABTests: { 'thefilter-at-a-glance-redesign': 'stacked' }, + serverSideABTests: { + 'thefilter-at-a-glance-redesign': 'stacked-default', + }, renderingTarget: 'Web', filterAtAGlanceEnabled: false, })(input); - const stacked = findStacked(output); + const stackedDefault = findStackedDefault(output); - expect(stacked).toBeUndefined(); + expect(stackedDefault).toBeUndefined(); }); }); diff --git a/dotcom-rendering/src/model/enhance-product-summary.ts b/dotcom-rendering/src/model/enhance-product-summary.ts index 8f6d44f22d9..3e0289d15e4 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.ts @@ -9,7 +9,7 @@ import { generateId } from './enhance-H2s'; * further up the rendering pipeline. */ -export type ABTestVariant = 'carousel' | 'stacked'; +export type ABTestVariant = 'carousel' | 'stacked-default' | 'stacked-expanded'; /** * List of page IDs eligible for product carousel enhancement. @@ -60,7 +60,11 @@ const isEligibleForSummary = (pageId: string) => { }; const isCarouselOrStacked = (string: string) => { - return string === 'carousel' || string === 'stacked'; + return ( + string === 'carousel' || + string === 'stacked-default' || + string === 'stacked-expanded' + ); }; // Extract URLs from 'At a glance' section elements diff --git a/dotcom-rendering/src/types/content.ts b/dotcom-rendering/src/types/content.ts index 2b7c10cab2e..bf9f085f3ad 100644 --- a/dotcom-rendering/src/types/content.ts +++ b/dotcom-rendering/src/types/content.ts @@ -526,7 +526,7 @@ export interface ProductBlockElement { export interface ProductSummaryElement { _type: 'model.dotcomrendering.pageElements.ProductSummaryElement'; matchedProducts: ProductBlockElement[]; - variant: 'carousel' | 'stacked'; + variant: 'carousel' | 'stacked-default' | 'stacked-expanded'; } interface ProfileAtomBlockElement { From 89dcd45a998f031ece0a8e897128c4b7e3ac5f43 Mon Sep 17 00:00:00 2001 From: Emma Imber Date: Mon, 23 Mar 2026 17:02:13 +0000 Subject: [PATCH 2/4] Update test name so that data is separated out from the previous test --- ab-testing/config/abTests.ts | 4 ++-- .../src/model/enhance-product-summary.test.ts | 8 ++++---- dotcom-rendering/src/model/enhance-product-summary.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ab-testing/config/abTests.ts b/ab-testing/config/abTests.ts index 50d5eb4bf66..70e68eda613 100644 --- a/ab-testing/config/abTests.ts +++ b/ab-testing/config/abTests.ts @@ -69,11 +69,11 @@ const ABTests: ABTest[] = [ shouldForceMetricsCollection: false, }, { - name: "thefilter-at-a-glance-redesign", + name: "thefilter-at-a-glance-redesign-v2", description: "Testing redesigned at a glance component on The Filter articles", owners: ["thefilter.dev@guardian.co.uk"], - expirationDate: "2026-04-01", + expirationDate: "2026-05-06", type: "server", status: "ON", audienceSize: 0 / 100, diff --git a/dotcom-rendering/src/model/enhance-product-summary.test.ts b/dotcom-rendering/src/model/enhance-product-summary.test.ts index 48e6e5be162..93a9289cecc 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.test.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.test.ts @@ -223,7 +223,7 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, serverSideABTests: { - 'thefilter-at-a-glance-redesign': 'carousel', + 'thefilter-at-a-glance-redesign-v2': 'carousel', }, renderingTarget: 'Web', filterAtAGlanceEnabled: true, @@ -267,7 +267,7 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, serverSideABTests: { - 'thefilter-at-a-glance-redesign': 'stacked-default', + 'thefilter-at-a-glance-redesign-v2': 'stacked-default', }, renderingTarget: 'Web', filterAtAGlanceEnabled: true, @@ -311,7 +311,7 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, serverSideABTests: { - 'thefilter-at-a-glance-redesign': 'stacked-default', + 'thefilter-at-a-glance-redesign-v2': 'stacked-default', }, renderingTarget: 'Apps', filterAtAGlanceEnabled: true, @@ -355,7 +355,7 @@ describe('enhanceProductSummary', () => { const output = enhanceProductSummary({ pageId: allowedPageId, serverSideABTests: { - 'thefilter-at-a-glance-redesign': 'stacked-default', + 'thefilter-at-a-glance-redesign-v2': 'stacked-default', }, renderingTarget: 'Web', filterAtAGlanceEnabled: false, diff --git a/dotcom-rendering/src/model/enhance-product-summary.ts b/dotcom-rendering/src/model/enhance-product-summary.ts index 3e0289d15e4..b389068bf4f 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.ts @@ -206,7 +206,7 @@ export const enhanceProductSummary = }) => (elements: FEElement[]): FEElement[] => { const abTestVariant = - serverSideABTests?.['thefilter-at-a-glance-redesign']; + serverSideABTests?.['thefilter-at-a-glance-redesign-v2']; // do nothing if article is not on allow list / not in the test / variant is 'control' / renderingTarget is Apps / filterAtAGlance switch is OFF if ( From c1b76d782aef95df8eabbd6129832b0951602a5f Mon Sep 17 00:00:00 2001 From: Emma Imber Date: Mon, 23 Mar 2026 17:52:29 +0000 Subject: [PATCH 3/4] Add test for expanded variant --- .../src/model/enhance-product-summary.test.ts | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/dotcom-rendering/src/model/enhance-product-summary.test.ts b/dotcom-rendering/src/model/enhance-product-summary.test.ts index 93a9289cecc..88455f113b5 100644 --- a/dotcom-rendering/src/model/enhance-product-summary.test.ts +++ b/dotcom-rendering/src/model/enhance-product-summary.test.ts @@ -4,6 +4,7 @@ import { dividerElement, findCarousel, findStackedDefault, + findStackedExpanded, linkElement, productElement, textElement, @@ -234,7 +235,7 @@ describe('enhanceProductSummary', () => { expect(carousel).toBeDefined(); }); - it('enhances elements with a stacked product for allowlisted pages', () => { + it('enhances elements with a default stacked product component for allowlisted pages', () => { const allowedPageId = 'thefilter/test-article-example-for-product-summary'; @@ -278,6 +279,50 @@ describe('enhanceProductSummary', () => { expect(stackedDefault).toBeDefined(); }); + it('enhances elements with an expanded stacked product component for allowlisted pages', () => { + const allowedPageId = + 'thefilter/test-article-example-for-product-summary'; + + const input = [ + atAGlanceHeading(), + linkElement( + 'https://www.homebase.co.uk/en-uk/tower-airx-t17166-5l-grey-single-basket-air-fryer-digital-air-fryer/p/0757395', + 'Buy now', + ), + linkElement( + 'https://www.lakeland.co.uk/27537/lakeland-slimline-air-fryer-black-8l', + 'Buy now', + ), + linkElement( + 'https://ninjakitchen.co.uk/product/ninja-double-stack-xl-9-5l-air-fryer-sl400uk-zidSL400UK', + 'Buy now', + ), + dividerElement(), + productElement([ + 'https://www.homebase.co.uk/en-uk/tower-airx-t17166-5l-grey-single-basket-air-fryer-digital-air-fryer/p/0757395', + ]), + productElement([ + 'https://www.lakeland.co.uk/27537/lakeland-slimline-air-fryer-black-8l', + ]), + productElement([ + 'https://ninjakitchen.co.uk/product/ninja-double-stack-xl-9-5l-air-fryer-sl400uk-zidSL400UK', + ]), + ]; + + const output = enhanceProductSummary({ + pageId: allowedPageId, + serverSideABTests: { + 'thefilter-at-a-glance-redesign-v2': 'stacked-expanded', + }, + renderingTarget: 'Web', + filterAtAGlanceEnabled: true, + })(input); + + const stackedExpanded = findStackedExpanded(output); + + expect(stackedExpanded).toBeDefined(); + }); + it('does not return stacked cards when the rendering target is apps', () => { const allowedPageId = 'thefilter/test-article-example-for-product-summary'; From b3441bf360b17abf5bbc65a00c39d3216cdc72ac Mon Sep 17 00:00:00 2001 From: Emma Imber Date: Tue, 24 Mar 2026 12:17:33 +0000 Subject: [PATCH 4/4] Generate new schemas --- dotcom-rendering/src/frontend/schemas/feArticle.json | 3 ++- dotcom-rendering/src/model/block-schema.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dotcom-rendering/src/frontend/schemas/feArticle.json b/dotcom-rendering/src/frontend/schemas/feArticle.json index 103a9fd1ccc..b3f1e4f6af8 100644 --- a/dotcom-rendering/src/frontend/schemas/feArticle.json +++ b/dotcom-rendering/src/frontend/schemas/feArticle.json @@ -4774,7 +4774,8 @@ "variant": { "enum": [ "carousel", - "stacked" + "stacked-default", + "stacked-expanded" ], "type": "string" } diff --git a/dotcom-rendering/src/model/block-schema.json b/dotcom-rendering/src/model/block-schema.json index 689661d7f7c..87de4403474 100644 --- a/dotcom-rendering/src/model/block-schema.json +++ b/dotcom-rendering/src/model/block-schema.json @@ -4256,7 +4256,8 @@ "variant": { "enum": [ "carousel", - "stacked" + "stacked-default", + "stacked-expanded" ], "type": "string" }