MM-63648 - markdown images sometimes do not show the more button (#30716)

* MM-63648 - markdown images sometimes do not show the more button

* migrate test to testing-library and remove unnecesary props

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Pablo Vélez
2025-05-01 11:20:41 +02:00
коммит произвёл GitHub
родитель 2decc2ccdb
Коммит 20f9f58e4c
3 изменённых файлов: 399 добавлений и 293 удалений

Просмотреть файл

@@ -1,231 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/post_view/ShowMore should match snapshot 1`] = `
<div
className="post-message post-message--collapsed"
>
<div>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": 200,
}
}
>
<div>
<p>
text
</p>
</div>
</div>
</div>
`;
exports[`components/post_view/ShowMore should match snapshot, PostAttachment on collapsed view 1`] = `
<div
className="post-message post-message--collapsed post-message--overflow"
>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": 200,
}
}
/>
<div
className="post-collapse"
class="post-message post-message--collapsed"
>
<div
className="post-attachment-collapse__show-more"
class="post-message__text-container"
style="max-height: 200px;"
>
<div
className="post-collapse__show-more-line"
/>
<button
className="post-collapse__show-more-button"
id="showMoreButton"
onClick={[Function]}
>
<span
className="fa fa-angle-down"
/>
<MemoizedFormattedMessage
defaultMessage="Show more"
id="post_info.message.show_more"
/>
</button>
<div
className="post-collapse__show-more-line"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/ShowMore should match snapshot, PostAttachment on expanded view 1`] = `
<div
className="post-message post-message--expanded post-message--overflow"
>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": undefined,
}
}
/>
<div
className="post-collapse"
>
<div
className="post-attachment-collapse__show-more"
>
<div
className="post-collapse__show-more-line"
/>
<button
className="post-collapse__show-more-button"
id="showMoreButton"
onClick={[Function]}
>
<span
className="fa fa-angle-up"
/>
<MemoizedFormattedMessage
defaultMessage="Show less"
id="post_info.message.show_less"
/>
</button>
<div
className="post-collapse__show-more-line"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/ShowMore should match snapshot, PostMessageView on collapsed view 1`] = `
<div
className="post-message post-message--collapsed post-message--overflow"
>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": 200,
}
}
/>
<div
className="post-collapse"
>
<div
className="post-collapse__show-more"
>
<div
className="post-collapse__show-more-line"
/>
<button
className="post-collapse__show-more-button"
id="showMoreButton"
onClick={[Function]}
>
<span
className="fa fa-angle-down"
/>
<MemoizedFormattedMessage
defaultMessage="Show more"
id="post_info.message.show_more"
/>
</button>
<div
className="post-collapse__show-more-line"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/ShowMore should match snapshot, PostMessageView on expanded view 1`] = `
<div
className="post-message post-message--expanded post-message--overflow"
>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": undefined,
}
}
/>
<div
className="post-collapse"
>
<div
className="post-collapse__show-more"
>
<div
className="post-collapse__show-more-line"
/>
<button
className="post-collapse__show-more-button"
id="showMoreButton"
onClick={[Function]}
>
<span
className="fa fa-angle-up"
/>
<MemoizedFormattedMessage
defaultMessage="Show less"
id="post_info.message.show_less"
/>
</button>
<div
className="post-collapse__show-more-line"
/>
</div>
</div>
</div>
`;
exports[`components/post_view/ShowMore should match snapshot, PostMessageView on expanded view with compactDisplay 1`] = `
<div
className="post-message post-message--expanded post-message--overflow"
>
<div
className="post-message__text-container"
style={
Object {
"maxHeight": undefined,
}
}
/>
<div
className="post-collapse"
>
<div
className="post-collapse__show-more"
>
<div
className="post-collapse__show-more-line"
/>
<button
className="post-collapse__show-more-button"
id="showMoreButton"
onClick={[Function]}
>
<span
className="fa fa-angle-up"
/>
<MemoizedFormattedMessage
defaultMessage="Show less"
id="post_info.message.show_less"
/>
</button>
<div
className="post-collapse__show-more-line"
/>
<div>
<p>
text
</p>
</div>
</div>
</div>
</div>

Просмотреть файл

@@ -1,102 +1,397 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
import React from 'react';
import ShowMore from 'components/post_view/show_more/show_more';
import {renderWithContext, screen, fireEvent, act} from 'tests/react_testing_utils';
describe('components/post_view/ShowMore', () => {
const children = (<div><p>{'text'}</p></div>);
const baseProps = {
checkOverflow: 0,
isAttachmentText: false,
isRHSExpanded: false,
isRHSOpen: false,
maxHeight: 200,
text: 'text',
compactDisplay: false,
};
// Helper function to mock the text container's scrollHeight
const mockTextContainerScrollHeight = (scrollHeight: number) => {
// Mock the scrollHeight property
Object.defineProperty(HTMLElement.prototype, 'scrollHeight', {
configurable: true,
value: scrollHeight,
});
};
// Helper function to restore the original scrollHeight behavior
const restoreTextContainerScrollHeight = () => {
// Delete the mocked scrollHeight property
delete (HTMLElement.prototype as any).scrollHeight;
};
afterEach(() => {
restoreTextContainerScrollHeight();
});
test('should match snapshot', () => {
const wrapper = shallow(<ShowMore {...baseProps}>{children}</ShowMore>);
expect(wrapper).toMatchSnapshot();
const {container} = renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
expect(container).toMatchSnapshot();
});
test('should match snapshot, PostMessageView on collapsed view', () => {
const wrapper = shallow(<ShowMore {...baseProps}/>);
wrapper.setState({isOverflow: true, isCollapsed: true});
expect(wrapper).toMatchSnapshot();
test('should render collapsed view when content overflows', () => {
// Setup fake timers
jest.useFakeTimers();
try {
// Mock scrollHeight to be greater than maxHeight to simulate overflow
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
const {container} = renderWithContext(
<ShowMore {...baseProps}>
<div style={{height: '300px'}}>{'Tall content that will overflow'}</div>
</ShowMore>,
);
// Manually trigger the overflow check
act(() => {
// Run the requestAnimationFrame callback
jest.runOnlyPendingTimers();
});
// Verify the "Show more" button is rendered
const showMoreButton = screen.getByText('Show more');
expect(showMoreButton).toBeInTheDocument();
// Verify the collapsed class is applied
expect(container.querySelector('.post-message--collapsed')).toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('should match snapshot, PostMessageView on expanded view', () => {
const wrapper = shallow(<ShowMore {...baseProps}/>);
wrapper.setState({isOverflow: true, isCollapsed: false});
expect(wrapper).toMatchSnapshot();
test('should render expanded view when show more button is clicked', () => {
// Setup fake timers
jest.useFakeTimers();
try {
// Mock scrollHeight to be greater than maxHeight to simulate overflow
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
const {container} = renderWithContext(
<ShowMore {...baseProps}>
<div style={{height: '300px'}}>{'Tall content that will overflow'}</div>
</ShowMore>,
);
// Manually trigger the overflow check
act(() => {
// Run the requestAnimationFrame callback
jest.runOnlyPendingTimers();
});
// Find and click the "Show more" button
const showMoreButton = screen.getByText('Show more');
fireEvent.click(showMoreButton);
// Verify the "Show less" button is now rendered
const showLessButton = screen.getByText('Show less');
expect(showLessButton).toBeInTheDocument();
// Verify the expanded class is applied
expect(container.querySelector('.post-message--expanded')).toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('should match snapshot, PostAttachment on collapsed view', () => {
const wrapper = shallow(
<ShowMore
{...baseProps}
isAttachmentText={true}
/>,
);
wrapper.setState({isOverflow: true, isCollapsed: true});
expect(wrapper).toMatchSnapshot();
test('should render attachment text in collapsed view', () => {
// Setup fake timers
jest.useFakeTimers();
try {
// Mock scrollHeight to be greater than maxHeight to simulate overflow
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
const {container} = renderWithContext(
<ShowMore
{...baseProps}
isAttachmentText={true}
>
<div style={{height: '300px'}}>{'Attachment text that will overflow'}</div>
</ShowMore>,
);
// Manually trigger the overflow check
act(() => {
// Run the requestAnimationFrame callback
jest.runOnlyPendingTimers();
});
// Verify the "Show more" button is rendered
const showMoreButton = screen.getByText('Show more');
expect(showMoreButton).toBeInTheDocument();
// Verify the attachment-specific class is applied
expect(container.querySelector('.post-attachment-collapse__show-more')).toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('should match snapshot, PostAttachment on expanded view', () => {
const wrapper = shallow(
<ShowMore
{...baseProps}
isAttachmentText={true}
/>,
);
wrapper.setState({isOverflow: true, isCollapsed: false});
expect(wrapper).toMatchSnapshot();
test('should render with compactDisplay', () => {
// Setup fake timers
jest.useFakeTimers();
try {
// Mock scrollHeight to be greater than maxHeight to simulate overflow
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
const {container} = renderWithContext(
<ShowMore
{...baseProps}
compactDisplay={true}
>
<div style={{height: '300px'}}>{'Content with compact display'}</div>
</ShowMore>,
);
// Manually trigger the overflow check
act(() => {
// Run the requestAnimationFrame callback
jest.runOnlyPendingTimers();
});
// Expand the content
const showMoreButton = screen.getByText('Show more');
fireEvent.click(showMoreButton);
// Verify the component renders correctly with compact display
expect(container.querySelector('.post-message--expanded')).toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
test('should match snapshot, PostMessageView on expanded view with compactDisplay', () => {
const wrapper = shallow(
<ShowMore
{...baseProps}
compactDisplay={true}
/>,
);
wrapper.setState({isOverflow: true, isCollapsed: false});
expect(wrapper).toMatchSnapshot();
test('should check overflow only when text or checkOverflow props change', () => {
// Create a spy for requestAnimationFrame
const originalRAF = window.requestAnimationFrame;
const rafSpy = jest.fn((cb) => {
cb(0);
return 0;
});
window.requestAnimationFrame = rafSpy;
try {
// Initial render with no overflow
mockTextContainerScrollHeight(baseProps.maxHeight - 50);
const {rerender} = renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
// Reset the RAF spy count
rafSpy.mockClear();
// Change props that SHOULD trigger overflow check
rafSpy.mockClear();
rerender(
<ShowMore
{...baseProps}
text={'text change'}
>{children}</ShowMore>,
);
expect(rafSpy).toHaveBeenCalled();
rafSpy.mockClear();
rerender(
<ShowMore
{...baseProps}
text={'text another change'}
>{children}</ShowMore>,
);
expect(rafSpy).toHaveBeenCalled();
rafSpy.mockClear();
rerender(
<ShowMore
{...baseProps}
checkOverflow={1}
>{children}</ShowMore>,
);
expect(rafSpy).toHaveBeenCalled();
// Same checkOverflow value should not trigger another check
rafSpy.mockClear();
rerender(
<ShowMore
{...baseProps}
checkOverflow={1}
>{children}</ShowMore>,
);
expect(rafSpy).not.toHaveBeenCalled();
} finally {
// Restore original requestAnimationFrame
window.requestAnimationFrame = originalRAF;
}
});
test('should call checkTextOverflow', () => {
const wrapper = shallow(<ShowMore {...baseProps}/>);
const instance = wrapper.instance() as ShowMore;
instance.checkTextOverflow = jest.fn();
describe('ResizeObserver functionality', () => {
let originalResizeObserver: any;
expect(instance.checkTextOverflow).not.toBeCalled();
beforeEach(() => {
// Store original implementation
originalResizeObserver = window.ResizeObserver;
wrapper.setProps({isRHSExpanded: true});
expect(instance.checkTextOverflow).toBeCalledTimes(1);
// Setup fake timers for requestAnimationFrame
jest.useFakeTimers();
});
wrapper.setProps({isRHSExpanded: false});
expect(instance.checkTextOverflow).toBeCalledTimes(2);
afterEach(() => {
// Restore original implementation
window.ResizeObserver = originalResizeObserver;
wrapper.setProps({isRHSOpen: true});
expect(instance.checkTextOverflow).toBeCalledTimes(3);
// Restore real timers
jest.useRealTimers();
});
wrapper.setProps({isRHSOpen: false});
expect(instance.checkTextOverflow).toBeCalledTimes(4);
test('should set up ResizeObserver on mount', () => {
// Track observer creation
const observeMock = jest.fn();
const disconnectMock = jest.fn();
wrapper.setProps({text: 'text change'});
expect(instance.checkTextOverflow).toBeCalledTimes(5);
// Mock ResizeObserver
window.ResizeObserver = jest.fn().mockImplementation(() => ({
observe: observeMock,
disconnect: disconnectMock,
})) as unknown as typeof ResizeObserver;
wrapper.setProps({text: 'text another change'});
expect(instance.checkTextOverflow).toBeCalledTimes(6);
// Render component
renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
wrapper.setProps({checkOverflow: 1});
expect(instance.checkTextOverflow).toBeCalledTimes(7);
// Verify ResizeObserver was created
expect(window.ResizeObserver).toHaveBeenCalled();
wrapper.setProps({checkOverflow: 1});
expect(instance.checkTextOverflow).toBeCalledTimes(7);
// Verify observe was called (meaning the text container is being observed)
expect(observeMock).toHaveBeenCalled();
});
test('should check overflow when ResizeObserver detects size changes', () => {
// Create a mock ResizeObserver that can be triggered manually
let resizeCallback: ResizeObserverCallback | undefined;
window.ResizeObserver = jest.fn().mockImplementation((callback) => {
resizeCallback = callback;
return {
observe: jest.fn(),
disconnect: jest.fn(),
};
}) as unknown as typeof ResizeObserver;
// Mock requestAnimationFrame to execute callback immediately
const originalRAF = window.requestAnimationFrame;
window.requestAnimationFrame = (cb) => {
cb(0);
return 0;
};
try {
// Mock scrollHeight to be greater than maxHeight after resize
mockTextContainerScrollHeight(baseProps.maxHeight - 50);
// Render component
const {container} = renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
// Verify no overflow initially
expect(container.querySelector('.post-message--overflow')).not.toBeInTheDocument();
// Now simulate a resize that causes overflow
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
// Trigger the ResizeObserver callback
if (resizeCallback && container.querySelector('.post-message__text-container')) {
const mockEntry = [{
target: container.querySelector('.post-message__text-container') as Element,
contentRect: {} as DOMRectReadOnly,
borderBoxSize: [] as ResizeObserverSize[],
contentBoxSize: [] as ResizeObserverSize[],
devicePixelContentBoxSize: [] as ResizeObserverSize[],
}];
act(() => {
resizeCallback!(mockEntry, {} as ResizeObserver);
});
}
// Verify overflow is detected
expect(container.querySelector('.post-message--overflow')).toBeInTheDocument();
} finally {
// Restore original requestAnimationFrame
window.requestAnimationFrame = originalRAF;
}
});
test('should clean up ResizeObserver on unmount', () => {
// Create mock with disconnect spy
const disconnectSpy = jest.fn();
window.ResizeObserver = jest.fn(() => ({
observe: jest.fn(),
disconnect: disconnectSpy,
})) as unknown as typeof ResizeObserver;
// Render and unmount component
const {unmount} = renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
unmount();
// Verify disconnect was called
expect(disconnectSpy).toHaveBeenCalled();
});
test('should handle browsers without ResizeObserver support', () => {
// Remove ResizeObserver
delete (window as any).ResizeObserver;
// Render component should not throw error
expect(() => {
renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
}).not.toThrow();
});
test('should update isOverflow state when content height changes', () => {
// Mock ResizeObserver before rendering
window.ResizeObserver = jest.fn().mockImplementation(() => {
return {
observe: jest.fn(),
disconnect: jest.fn(),
};
}) as unknown as typeof ResizeObserver;
// Mock requestAnimationFrame to execute callback immediately
const originalRAF = window.requestAnimationFrame;
window.requestAnimationFrame = (cb) => {
cb(0);
return 0;
};
try {
// Initial render with no overflow
mockTextContainerScrollHeight(baseProps.maxHeight - 50);
const {container, rerender} = renderWithContext(<ShowMore {...baseProps}>{children}</ShowMore>);
// Verify no overflow initially
expect(container.querySelector('.post-message--overflow')).not.toBeInTheDocument();
// Now simulate content that overflows
mockTextContainerScrollHeight(baseProps.maxHeight + 50);
// Force a re-render to trigger checkTextOverflow
rerender(
<ShowMore
{...baseProps}
checkOverflow={1}
>{children}</ShowMore>,
);
// Verify overflow is detected
expect(container.querySelector('.post-message--overflow')).toBeInTheDocument();
} finally {
// Restore original requestAnimationFrame
window.requestAnimationFrame = originalRAF;
}
});
});
});

Просмотреть файл

@@ -13,8 +13,6 @@ type Props = {
children?: React.ReactNode;
checkOverflow?: number;
isAttachmentText?: boolean;
isRHSExpanded: boolean;
isRHSOpen: boolean;
text?: string;
compactDisplay: boolean;
overflowType?: AttachmentTextOverflowType;
@@ -30,6 +28,7 @@ export default class ShowMore extends React.PureComponent<Props, State> {
private maxHeight: number;
private textContainer: React.RefObject<HTMLDivElement>;
private overflowRef?: number;
private resizeObserver: ResizeObserver | null = null;
constructor(props: Props) {
super(props);
@@ -42,16 +41,17 @@ export default class ShowMore extends React.PureComponent<Props, State> {
}
componentDidMount() {
this.checkTextOverflow();
this.setupResizeObserver();
window.addEventListener('resize', this.handleResize);
// Initial check for overflow
this.checkTextOverflow();
}
componentDidUpdate(prevProps: Props) {
// Only manually check for overflow when text content changes or when explicitly requested
// ResizeObserver will handle size changes caused by other factors
if (
this.props.text !== prevProps.text ||
this.props.isRHSExpanded !== prevProps.isRHSExpanded ||
this.props.isRHSOpen !== prevProps.isRHSOpen ||
this.props.checkOverflow !== prevProps.checkOverflow
) {
this.checkTextOverflow();
@@ -59,12 +59,39 @@ export default class ShowMore extends React.PureComponent<Props, State> {
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
if (this.overflowRef) {
window.cancelAnimationFrame(this.overflowRef);
}
this.cleanupResizeObserver();
}
setupResizeObserver = () => {
if (!this.textContainer.current || !window.ResizeObserver) {
// ResizeObserver is not supported in this browser or the container is not available yet
return;
}
// Clean up any existing observer before creating a new one
// This prevents multiple observers in case setupResizeObserver is called more than once
this.cleanupResizeObserver();
// Create a new ResizeObserver to watch for size changes in the text container
this.resizeObserver = new ResizeObserver(() => {
// When the size of the text container changes, check if we need to show/hide the "Show More" button
this.checkTextOverflow();
});
// Start observing the text container
this.resizeObserver.observe(this.textContainer.current);
};
cleanupResizeObserver = () => {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
};
toggleCollapse = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
@@ -94,10 +121,6 @@ export default class ShowMore extends React.PureComponent<Props, State> {
});
};
handleResize = () => {
this.checkTextOverflow();
};
render() {
const {
isCollapsed,