From 7457c8c76585ab0b2470cb7a123d9ab1653ec378 Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Fri, 31 Oct 2025 12:15:22 -0700 Subject: [PATCH 1/6] issue#68721 --- src/components/DisplayNames/index.native.tsx | 9 +- src/components/DisplayNames/index.tsx | 5 +- src/components/DisplayNames/types.ts | 3 + src/pages/ReportDetailsPage.tsx | 4 +- .../unit/DisplayNamesShouldParseHTMLTest.tsx | 105 ++++++++++++++++++ 5 files changed, 120 insertions(+), 6 deletions(-) create mode 100644 tests/unit/DisplayNamesShouldParseHTMLTest.tsx diff --git a/src/components/DisplayNames/index.native.tsx b/src/components/DisplayNames/index.native.tsx index cae91c80272e..ab0e659cdb6f 100644 --- a/src/components/DisplayNames/index.native.tsx +++ b/src/components/DisplayNames/index.native.tsx @@ -8,8 +8,11 @@ import TextWithEmojiFragment from '@pages/home/report/comment/TextWithEmojiFragm import type DisplayNamesProps from './types'; // As we don't have to show tooltips of the Native platform so we simply render the full display names list. -function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1, renderAdditionalText, forwardedFSClass, testID}: DisplayNamesProps) { +function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1, renderAdditionalText, forwardedFSClass, testID, shouldParseHtml = false}: DisplayNamesProps) { const {translate} = useLocalize(); + const title = shouldParseHtml + ? StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden') + : StringUtils.lineBreaksToSpaces(fullTitle) || translate('common.hidden'); const titleContainsTextAndCustomEmoji = useMemo(() => containsCustomEmoji(fullTitle) && !containsOnlyCustomEmoji(fullTitle), [fullTitle]); return ( {titleContainsTextAndCustomEmoji ? ( ) : ( - StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden') + title )} {renderAdditionalText?.()} diff --git a/src/components/DisplayNames/index.tsx b/src/components/DisplayNames/index.tsx index 5158747e4c25..5e1a82104501 100644 --- a/src/components/DisplayNames/index.tsx +++ b/src/components/DisplayNames/index.tsx @@ -16,9 +16,12 @@ function DisplayNames({ displayNamesWithTooltips, renderAdditionalText, forwardedFSClass, + shouldParseHtml = false, }: DisplayNamesProps) { const {translate} = useLocalize(); - const title = StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden'); + const title = shouldParseHtml + ? StringUtils.lineBreaksToSpaces(Parser.htmlToText(fullTitle)) || translate('common.hidden') + : StringUtils.lineBreaksToSpaces(fullTitle) || translate('common.hidden'); if (!tooltipEnabled) { return ( diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 687e14fa6ef0..9da0b69e48ac 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -49,6 +49,9 @@ type DisplayNamesProps = ForwardedFSClassProps & { /** TestID indicating order */ testID?: number; + + /** Whether to parse HTML in the fullTitle. Defaults to true for backward compatibility */ + shouldParseHtml?: boolean; }; export default DisplayNamesProps; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e45f79b76e0f..ff27161a722b 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -331,8 +331,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, currentUserPersonalDetails?.email) && !policy?.isJoinRequestPending; - - const reportName = Parser.htmlToText(getReportName(report, undefined, undefined, undefined, undefined, reportAttributes)); + const reportName = getReportName(report, undefined, undefined, undefined, reportAttributes); const additionalRoomDetails = (isPolicyExpenseChat && !!report?.isOwnPolicyExpenseChat) || isExpenseReportUtil(report) || isPolicyExpenseChat || isInvoiceRoom @@ -677,6 +676,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail numberOfLines={isChatRoom && !isChatThread ? 0 : 1} textStyles={[styles.textHeadline, styles.textAlignCenter, isChatRoom && !isChatThread ? undefined : styles.pre]} shouldUseFullTitle={shouldUseFullTitle} + shouldParseHtml /> {isPolicyAdmin ? ( diff --git a/tests/unit/DisplayNamesShouldParseHTMLTest.tsx b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx new file mode 100644 index 000000000000..b62c4c8ae9f6 --- /dev/null +++ b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx @@ -0,0 +1,105 @@ +import {render} from '@testing-library/react-native'; +import React from 'react'; +import DisplayNames from '@components/DisplayNames'; + +jest.mock('@hooks/useLocalize', () => ({ + __esModule: true, + default: () => ({ + translate: jest.fn((key) => { + if (key === 'common.hidden') { + return 'hidden'; + } + return key; + }), + }), +})); + +jest.mock('@libs/Parser', () => ({ + __esModule: true, + default: { + htmlToText: jest.fn((html: string) => { + // Simulate stripTag behavior: remove anything that looks like HTML tags + return html.replace(/(<([^>]+)>)/gi, ''); + }), + }, +})); + +jest.mock('@libs/StringUtils', () => ({ + __esModule: true, + default: { + lineBreaksToSpaces: jest.fn((text: string) => text), + }, +})); + +describe('DisplayNames - shouldParseHtml prop', () => { + const testTitle = '< >'; + const defaultProps = { + fullTitle: testTitle, + numberOfLines: 1, + tooltipEnabled: false, + }; + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should NOT parse HTML by default (shouldParseHtml defaults to false)', () => { + const {getByText} = render(); + + // With shouldParseHtml = false (default), "< >" should be preserved + expect(getByText('< >')).toBeTruthy(); + }); + + it('should parse HTML when shouldParseHtml is explicitly set to true', () => { + const htmlTitle = 'Bold Text'; + const {getByText, queryByText} = render( + , + ); + + // With shouldParseHtml = true, HTML tags should be stripped + expect(getByText('Bold Text')).toBeTruthy(); + expect(queryByText('Bold Text')).toBeNull(); + }); + + it('should preserve special characters when shouldParseHtml is false', () => { + const specialCharsTitle = '< > & " \' test'; + const {getByText} = render( + , + ); + + // Special characters should be preserved when not parsing HTML + expect(getByText(specialCharsTitle)).toBeTruthy(); + }); + + it('should show "hidden" when title is empty and HTML parsing is disabled', () => { + const {getByText} = render( + , + ); + + expect(getByText('hidden')).toBeTruthy(); + }); + + it('should show "hidden" when title becomes empty after HTML parsing', () => { + const onlyTagsTitle = '
'; + const {getByText} = render( + , + ); + + // After parsing, only tags remain which get stripped to empty string + expect(getByText('hidden')).toBeTruthy(); + }); +}); From 8d46f51be5b4ff290e676c1dde83b978e503047c Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Wed, 5 Nov 2025 18:54:55 -0800 Subject: [PATCH 2/6] refactored redundant prop --- src/pages/ReportDetailsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index ff27161a722b..e0c08d7fb931 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -676,7 +676,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail numberOfLines={isChatRoom && !isChatThread ? 0 : 1} textStyles={[styles.textHeadline, styles.textAlignCenter, isChatRoom && !isChatThread ? undefined : styles.pre]} shouldUseFullTitle={shouldUseFullTitle} - shouldParseHtml /> {isPolicyAdmin ? ( From 4b0d802925827d458a13a934bdfac8c1478f0416 Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Wed, 5 Nov 2025 18:57:42 -0800 Subject: [PATCH 3/6] Update src/components/DisplayNames/types.ts left over from refactor comment is no longer accurate Co-authored-by: Qichen Zhu --- src/components/DisplayNames/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 9da0b69e48ac..9463c810229c 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -50,7 +50,7 @@ type DisplayNamesProps = ForwardedFSClassProps & { /** TestID indicating order */ testID?: number; - /** Whether to parse HTML in the fullTitle. Defaults to true for backward compatibility */ + /** Whether to parse HTML in the fullTitle */ shouldParseHtml?: boolean; }; From 3905bf74ba48c362ed3c1ac20e2f4f442ec9403d Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Wed, 5 Nov 2025 18:59:09 -0800 Subject: [PATCH 4/6] Update src/pages/ReportDetailsPage.tsx refactoring based upon correct pattern usage Co-authored-by: Qichen Zhu --- src/pages/ReportDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e0c08d7fb931..0be09dc00a24 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -331,7 +331,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, currentUserPersonalDetails?.email) && !policy?.isJoinRequestPending; - const reportName = getReportName(report, undefined, undefined, undefined, reportAttributes); + const reportName = getReportName(report, undefined, undefined, undefined, undefined, reportAttributes); const additionalRoomDetails = (isPolicyExpenseChat && !!report?.isOwnPolicyExpenseChat) || isExpenseReportUtil(report) || isPolicyExpenseChat || isInvoiceRoom From caf1fbbd89d58f00d3ccd7cb528465e4af4e7634 Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Wed, 5 Nov 2025 21:35:29 -0800 Subject: [PATCH 5/6] fixed linting errors in test files and unused import that was breaking github action processes --- src/pages/ReportDetailsPage.tsx | 1 - .../unit/DisplayNamesShouldParseHTMLTest.tsx | 56 ++++++------------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 0be09dc00a24..195b77ebe36d 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -41,7 +41,6 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; -import Parser from '@libs/Parser'; import Permissions from '@libs/Permissions'; import {isPolicyAdmin as isPolicyAdminUtil, isPolicyEmployee as isPolicyEmployeeUtil, shouldShowPolicy} from '@libs/PolicyUtils'; import {getOneTransactionThreadReportID, getOriginalMessage, getTrackExpenseActionableWhisper, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; diff --git a/tests/unit/DisplayNamesShouldParseHTMLTest.tsx b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx index b62c4c8ae9f6..0a91543509e3 100644 --- a/tests/unit/DisplayNamesShouldParseHTMLTest.tsx +++ b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx @@ -1,11 +1,12 @@ -import {render} from '@testing-library/react-native'; +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) => { + translate: jest.fn((key: string): string => { if (key === 'common.hidden') { return 'hidden'; } @@ -15,6 +16,7 @@ jest.mock('@hooks/useLocalize', () => ({ })); jest.mock('@libs/Parser', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, default: { htmlToText: jest.fn((html: string) => { @@ -25,6 +27,7 @@ jest.mock('@libs/Parser', () => ({ })); jest.mock('@libs/StringUtils', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention __esModule: true, default: { lineBreaksToSpaces: jest.fn((text: string) => text), @@ -33,73 +36,46 @@ jest.mock('@libs/StringUtils', () => ({ describe('DisplayNames - shouldParseHtml prop', () => { const testTitle = '< >'; - const defaultProps = { - fullTitle: testTitle, - numberOfLines: 1, - tooltipEnabled: false, - }; afterEach(() => { jest.clearAllMocks(); }); it('should NOT parse HTML by default (shouldParseHtml defaults to false)', () => { - const {getByText} = render(); + render(); // With shouldParseHtml = false (default), "< >" should be preserved - expect(getByText('< >')).toBeTruthy(); + expect(screen.getByText('< >')).toBeTruthy(); }); it('should parse HTML when shouldParseHtml is explicitly set to true', () => { const htmlTitle = 'Bold Text'; - const {getByText, queryByText} = render( - , - ); + render(); // With shouldParseHtml = true, HTML tags should be stripped - expect(getByText('Bold Text')).toBeTruthy(); - expect(queryByText('Bold Text')).toBeNull(); + expect(screen.getByText('Bold Text')).toBeTruthy(); + expect(screen.queryByText('Bold Text')).toBeNull(); }); it('should preserve special characters when shouldParseHtml is false', () => { const specialCharsTitle = '< > & " \' test'; - const {getByText} = render( - , - ); + render(); // Special characters should be preserved when not parsing HTML - expect(getByText(specialCharsTitle)).toBeTruthy(); + expect(screen.getByText(specialCharsTitle)).toBeTruthy(); }); it('should show "hidden" when title is empty and HTML parsing is disabled', () => { - const {getByText} = render( - , - ); + render(); - expect(getByText('hidden')).toBeTruthy(); + expect(screen.getByText('hidden')).toBeTruthy(); }); it('should show "hidden" when title becomes empty after HTML parsing', () => { const onlyTagsTitle = '
'; - const {getByText} = render( - , - ); + render(); // After parsing, only tags remain which get stripped to empty string - expect(getByText('hidden')).toBeTruthy(); + expect(screen.getByText('hidden')).toBeTruthy(); }); }); From 504a06b2fa7f23e6deb9e15209b8d340df5a0469 Mon Sep 17 00:00:00 2001 From: Trace Harris Date: Thu, 6 Nov 2025 14:43:43 -0800 Subject: [PATCH 6/6] ran npm run prettier and this formatted tests/unit/DisplayNamesShouldParseHTMLTest.tsx --- .../unit/DisplayNamesShouldParseHTMLTest.tsx | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/tests/unit/DisplayNamesShouldParseHTMLTest.tsx b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx index 0a91543509e3..1103aab38677 100644 --- a/tests/unit/DisplayNamesShouldParseHTMLTest.tsx +++ b/tests/unit/DisplayNamesShouldParseHTMLTest.tsx @@ -42,7 +42,13 @@ describe('DisplayNames - shouldParseHtml prop', () => { }); it('should NOT parse HTML by default (shouldParseHtml defaults to false)', () => { - render(); + render( + , + ); // With shouldParseHtml = false (default), "< >" should be preserved expect(screen.getByText('< >')).toBeTruthy(); @@ -50,7 +56,14 @@ describe('DisplayNames - shouldParseHtml prop', () => { it('should parse HTML when shouldParseHtml is explicitly set to true', () => { const htmlTitle = 'Bold Text'; - render(); + render( + , + ); // With shouldParseHtml = true, HTML tags should be stripped expect(screen.getByText('Bold Text')).toBeTruthy(); @@ -59,21 +72,40 @@ describe('DisplayNames - shouldParseHtml prop', () => { it('should preserve special characters when shouldParseHtml is false', () => { const specialCharsTitle = '< > & " \' test'; - render(); + render( + , + ); // Special characters should be preserved when not parsing HTML expect(screen.getByText(specialCharsTitle)).toBeTruthy(); }); it('should show "hidden" when title is empty and HTML parsing is disabled', () => { - render(); + render( + , + ); expect(screen.getByText('hidden')).toBeTruthy(); }); it('should show "hidden" when title becomes empty after HTML parsing', () => { const onlyTagsTitle = '
'; - render(); + render( + , + ); // After parsing, only tags remain which get stripped to empty string expect(screen.getByText('hidden')).toBeTruthy();