Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/pages/home/HeaderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ConfirmModal from '@components/ConfirmModal';
import DisplayNames from '@components/DisplayNames';
import Icon from '@components/Icon';
import {BackArrow, DotIndicator} from '@components/Icon/Expensicons';

Check warning on line 11 in src/pages/home/HeaderView.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used by a pattern. Direct imports from Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details

Check warning on line 11 in src/pages/home/HeaderView.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used. Direct imports from @components/Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details
import LoadingBar from '@components/LoadingBar';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import OnboardingHelpDropdownButton from '@components/OnboardingHelpDropdownButton';
Expand Down Expand Up @@ -304,6 +304,7 @@
renderAdditionalText={renderAdditionalText}
shouldAddEllipsis={shouldAddEllipsis}
forwardedFSClass={displayNamesFSClass}
shouldParseHtml={isChatThread}
/>
</CaretWrapper>
{!isEmptyObject(parentNavigationSubtitleData) && (
Expand Down
164 changes: 164 additions & 0 deletions tests/unit/ThreadHeaderHTMLParsingTest.tsx

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have DisplayNamesShouldParseHTMLTest, so if any cases are not covered, please add them there and don't duplicate. Also, please refrain from adding comments if the code explains itself.

@whiletrace whiletrace Nov 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@QichenZhu I am hitting a blocker running the end to end tests due to the process of adding the mock accounts to test properly for the regression I am going to need to come back to this tomorrow fresh after my day job I can resume 11/19/25 16:00. I will update the branch with your requested revisions when I get back to this Note: We should be to solve #75470 in this PR as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay will do,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's urgent, mind if I take over to speed things up?

@whiletrace whiletrace Nov 19, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's urgent, mind if I take over to speed things up?

@QichenZhu I would be very grateful for your assistance.

Original file line number Diff line number Diff line change
@@ -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 <a href="https://example.com">workspace rules</a>';
render(
<DisplayNames
fullTitle={htmlTitle}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml
/>,
);

// 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 <Company>';
render(
<DisplayNames
fullTitle={titleWithBrackets}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml={false}
/>,
);

// With shouldParseHtml = false, angle brackets should be preserved
expect(screen.getByText('Test <Company>')).toBeTruthy();
});

it('should parse mention-user tags when shouldParseHtml is true', () => {
const htmlWithMention = 'changed the approver to <mention-user accountID="12345">@John Doe</mention-user>';
render(
<DisplayNames
fullTitle={htmlWithMention}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml
/>,
);

// 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 <a href="https://example.com/settings">workspace settings</a>.';
render(
<DisplayNames
fullTitle={integrationHTML}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml
/>,
);

// 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 <a href="https://example.com">workspace rules</a>',
expected: 'approved via workspace rules',
},
{
html: 'paid $100.00 with bank account 1234 via <a href="https://example.com">workspace rules</a>',
expected: 'paid $100.00 with bank account 1234 via workspace rules',
},
{
html: 'paid with Expensify via <a href="https://example.com">workspace rules</a>',
expected: 'paid with Expensify via workspace rules',
},
];

testCases.forEach(({html, expected}) => {
const {unmount} = render(
<DisplayNames
fullTitle={html}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml
/>,
);

expect(screen.getByText(expected)).toBeTruthy();
unmount();
});
});

it('should default to shouldParseHtml=false to preserve group chat names', () => {
const groupChatName = 'Engineering <Team>';
render(
<DisplayNames
fullTitle={groupChatName}
numberOfLines={1}
tooltipEnabled={false}
// Not passing shouldParseHtml, should default to false
/>,
);

// Angle brackets should be preserved with default behavior
expect(screen.getByText('Engineering <Team>')).toBeTruthy();
});

it('should show "hidden" when title becomes empty after HTML parsing', () => {
const onlyTagsTitle = '<div></div>';
render(
<DisplayNames
fullTitle={onlyTagsTitle}
numberOfLines={1}
tooltipEnabled={false}
shouldParseHtml
/>,
);

// After parsing, only tags remain which get stripped to empty string
expect(screen.getByText('hidden')).toBeTruthy();
});
});
Loading