From cfb4f8d5b4355b73403919e62496bea61fa346dd Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Tue, 18 Nov 2025 19:01:05 -0800 Subject: [PATCH 1/2] Expense header shows workspace rules in HTML --- src/pages/home/HeaderView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 096120625bc1..9ecda7654241 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -304,6 +304,7 @@ function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, renderAdditionalText={renderAdditionalText} shouldAddEllipsis={shouldAddEllipsis} forwardedFSClass={displayNamesFSClass} + shouldParseHtml={isChatThread} /> {!isEmptyObject(parentNavigationSubtitleData) && ( From 685c500ff18d60bf2070b50496aa69a5850409fc Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Tue, 18 Nov 2025 20:03:19 -0800 Subject: [PATCH 2/2] unit tests for regression fix --- tests/unit/ThreadHeaderHTMLParsingTest.tsx | 164 +++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 tests/unit/ThreadHeaderHTMLParsingTest.tsx diff --git a/tests/unit/ThreadHeaderHTMLParsingTest.tsx b/tests/unit/ThreadHeaderHTMLParsingTest.tsx new file mode 100644 index 000000000000..c6c2f15507a0 --- /dev/null +++ b/tests/unit/ThreadHeaderHTMLParsingTest.tsx @@ -0,0 +1,164 @@ +import {render, screen} from '@testing-library/react-native'; +import React from 'react'; +import DisplayNames from '@components/DisplayNames'; + +jest.mock('@hooks/useLocalize', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: () => ({ + translate: jest.fn((key: string): string => { + if (key === 'common.hidden') { + return 'hidden'; + } + return key; + }), + }), +})); + +jest.mock('@libs/Parser', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + htmlToText: jest.fn((html: string) => { + // Simulate stripTag behavior: remove anything that looks like HTML tags + return html.replaceAll(/(<([^>]+)>)/gi, ''); + }), + }, +})); + +jest.mock('@libs/StringUtils', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: { + lineBreaksToSpaces: jest.fn((text: string) => text), + }, +})); + +describe('DisplayNames - Thread Header HTML Parsing', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should parse HTML when shouldParseHtml is true (for thread headers)', () => { + const htmlTitle = 'approved via workspace rules'; + render( + , + ); + + // With shouldParseHtml = true, HTML tags should be stripped + expect(screen.getByText('approved via workspace rules')).toBeTruthy(); + expect(screen.queryByText(htmlTitle)).toBeNull(); + }); + + it('should NOT parse HTML when shouldParseHtml is false (for group chats)', () => { + const titleWithBrackets = 'Test '; + render( + , + ); + + // With shouldParseHtml = false, angle brackets should be preserved + expect(screen.getByText('Test ')).toBeTruthy(); + }); + + it('should parse mention-user tags when shouldParseHtml is true', () => { + const htmlWithMention = 'changed the approver to @John Doe'; + render( + , + ); + + // mention-user tags should be stripped + expect(screen.getByText('changed the approver to @John Doe')).toBeTruthy(); + expect(screen.queryByText(htmlWithMention)).toBeNull(); + }); + + it('should parse integration sync failed message HTML', () => { + const integrationHTML = 'there was a problem syncing. Please fix the issue in workspace settings.'; + render( + , + ); + + // HTML link should be stripped + expect(screen.getByText('there was a problem syncing. Please fix the issue in workspace settings.')).toBeTruthy(); + }); + + it('should parse multiple automatic workflow HTML messages', () => { + const testCases = [ + { + html: 'approved via workspace rules', + expected: 'approved via workspace rules', + }, + { + html: 'paid $100.00 with bank account 1234 via workspace rules', + expected: 'paid $100.00 with bank account 1234 via workspace rules', + }, + { + html: 'paid with Expensify via workspace rules', + expected: 'paid with Expensify via workspace rules', + }, + ]; + + testCases.forEach(({html, expected}) => { + const {unmount} = render( + , + ); + + expect(screen.getByText(expected)).toBeTruthy(); + unmount(); + }); + }); + + it('should default to shouldParseHtml=false to preserve group chat names', () => { + const groupChatName = 'Engineering '; + render( + , + ); + + // Angle brackets should be preserved with default behavior + expect(screen.getByText('Engineering ')).toBeTruthy(); + }); + + it('should show "hidden" when title becomes empty after HTML parsing', () => { + const onlyTagsTitle = '
'; + render( + , + ); + + // After parsing, only tags remain which get stripped to empty string + expect(screen.getByText('hidden')).toBeTruthy(); + }); +});