From e632488d7a05303de6c094b677b580aea1f1cb9e Mon Sep 17 00:00:00 2001 From: User Date: Wed, 10 Jun 2026 11:15:05 -0500 Subject: [PATCH 1/3] feat(DTCRCMERC-4687): implement v5 text-layout styles for renderV2Message - Add body reset and scoped .pp-message stylesheet (display:block, width:100%) - Map text.color to CSS classes on .main/.action spans with vertical-align:middle - Add CSS filters on .logo img for white (invert), monochrome (grayscale+black), grayscale variants - Handle logo positions: left (default), right (margin swap), top (block display) - Implement fontSource @font-face generation with URL/name security validation - Wire fontSource through message.jsx into styles() - Add data-pp-style-* root attributes for layout, logo-type, logo-position, text-align, text-color, text-size - Add greyscale alias pipeline test (validateStyle -> render -> class) - Expand snapshot coverage to all 7 text sizes (10-16) and full logo/color/align matrix (21 snapshots) --- src/server/v2/message.jsx | 11 +- src/server/v2/styles.js | 80 +- .../v2/__snapshots__/render.test.js.snap | 1240 +++++++++++++++++ tests/unit/spec/server/v2/render.test.js | 212 +++ 4 files changed, 1534 insertions(+), 9 deletions(-) create mode 100644 tests/unit/spec/server/v2/__snapshots__/render.test.js.snap diff --git a/src/server/v2/message.jsx b/src/server/v2/message.jsx index 58e5fb63d0..f235943724 100644 --- a/src/server/v2/message.jsx +++ b/src/server/v2/message.jsx @@ -53,12 +53,21 @@ export default function V2Message({ options, v2Content }) { const actionLabel = buildContentLabel(actionItems); return ( -
+
{/* eslint-disable react/no-danger */} Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo position: right 1`] = ` +"
Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo position: top 1`] = ` +"
Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo type: alternative 1`] = ` +"
Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo type: inline 1`] = ` +"
\\"PayPal\\"Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo type: none 1`] = ` +"
Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots logo type: primary 1`] = ` +"
Pay Later. Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text align: center 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text align: left 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text align: right 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text color: black 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text color: grayscale 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text color: monochrome 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text color: white 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 10px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 11px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 12px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 13px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 14px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 15px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; + +exports[`v2 render snapshots text size: 16px 1`] = ` +"
Pay Later Subject to approval.Learn more
" +`; diff --git a/tests/unit/spec/server/v2/render.test.js b/tests/unit/spec/server/v2/render.test.js index 842893e63b..fb27e8f7ee 100644 --- a/tests/unit/spec/server/v2/render.test.js +++ b/tests/unit/spec/server/v2/render.test.js @@ -1,4 +1,5 @@ import render from 'server/v2/render'; +import validateStyle from 'server/v2/validateStyle'; const mockLog = jest.fn(); @@ -201,6 +202,18 @@ describe('v2 render', () => { expect(result).toMatch(/class="main[^"]*white[^"]*"/); }); + test('greyscale alias normalizes to grayscale class after validation', () => { + const raw = { + layout: 'text', + logo: { type: 'primary', position: 'left' }, + text: { color: 'greyscale', size: 12 } + }; + const validatedStyle = validateStyle(mockLog, raw); + const result = render({ style: validatedStyle }, baseV2Content, mockLog); + expect(result).toMatch(/class="main[^"]*grayscale[^"]*"/); + expect(result).not.toContain('greyscale'); + }); + test('handles empty main_items gracefully', () => { const content = { ...baseV2Content, main_items: [] }; const result = render(baseOptions, content, mockLog); @@ -219,4 +232,203 @@ describe('v2 render', () => { expect(result).not.toContain('message__headline'); expect(result).not.toContain('message__foreground'); }); + + test('maps validated v5 text style options to root data attributes', () => { + const options = { + style: { + layout: 'text', + logo: { type: 'primary', position: 'top' }, + text: { align: 'center', color: 'white', size: 16 } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toContain('data-pp-style-layout="text"'); + expect(result).toContain('data-pp-style-logo-position="top"'); + expect(result).toContain('data-pp-style-logo-type="primary"'); + expect(result).toContain('data-pp-style-text-align="center"'); + expect(result).toContain('data-pp-style-text-color="white"'); + expect(result).toContain('data-pp-style-text-size="16"'); + }); +}); + +describe('v2 render fontSource', () => { + test('generates @font-face rule for a single fontSource URL', () => { + const options = { + style: { + ...baseOptions.style, + text: { color: 'black', size: 12, fontSource: ['https://example.com/font.woff2'] } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toContain('@font-face'); + expect(result).toContain("url('https://example.com/font.woff2')"); + expect(result).toContain("font-family: 'PP Merchant Font 1'"); + }); + + test('prepends custom font name to font-family stack', () => { + const options = { + style: { + ...baseOptions.style, + text: { color: 'black', size: 12, fontSource: ['https://example.com/font.woff2'] } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toMatch(/font-family:\s*'PP Merchant Font 1',/); + expect(result).not.toContain('"PayPal Pro"'); + }); + + test('generates one @font-face rule per fontSource URL', () => { + const options = { + style: { + ...baseOptions.style, + text: { + color: 'black', + size: 12, + fontSource: ['https://example.com/font1.woff2', 'https://example.com/font2.woff2'] + } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toContain("url('https://example.com/font1.woff2')"); + expect(result).toContain("url('https://example.com/font2.woff2')"); + expect(result).toContain("'PP Merchant Font 1'"); + expect(result).toContain("'PP Merchant Font 2'"); + }); + + test('includes explicit fontFamily after fontSource names in font-family stack', () => { + const options = { + style: { + ...baseOptions.style, + text: { + color: 'black', + size: 12, + fontSource: ['https://example.com/font.woff2'], + fontFamily: ['MyFont'] + } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toMatch(/'PP Merchant Font 1',\s*'MyFont',/); + }); + + test('includes explicit fontFamily without fontSource', () => { + const options = { + style: { + ...baseOptions.style, + text: { color: 'black', size: 12, fontFamily: ['Impact', 'sans-serif'] } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).toContain("font-family: 'Impact', sans-serif, Helvetica"); + }); + + test('uses PayPal Pro default font-family when fontSource is not provided', () => { + const result = render(baseOptions, baseV2Content, mockLog); + expect(result).not.toContain('@font-face'); + expect(result).toContain('"PayPal Pro"'); + }); + + test('uses PayPal Pro default font-family when fontSource is empty array', () => { + const options = { + style: { ...baseOptions.style, text: { color: 'black', size: 12, fontSource: [] } } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).not.toContain('@font-face'); + expect(result).toContain('"PayPal Pro"'); + }); + + test('ignores unsafe fontSource URLs', () => { + const options = { + style: { + ...baseOptions.style, + text: { color: 'black', size: 12, fontSource: ['./font.woff2'] } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).not.toContain('@font-face'); + expect(result).not.toContain('./font.woff2'); + }); + + test('ignores unsafe fontFamily values', () => { + const options = { + style: { + ...baseOptions.style, + text: { color: 'black', size: 12, fontFamily: [""] } + } + }; + const result = render(options, baseV2Content, mockLog); + expect(result).not.toContain('