[MM-64618] ResizeObserver callback fires after component unmount during channel switching (#32668)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
d2188ce1dd
Коммит
0d9c4810f4
@@ -188,11 +188,11 @@
|
|||||||
"build": "cross-env NODE_ENV=production webpack",
|
"build": "cross-env NODE_ENV=production webpack",
|
||||||
"run": "webpack --progress --watch",
|
"run": "webpack --progress --watch",
|
||||||
"dev-server": "webpack serve --mode development",
|
"dev-server": "webpack serve --mode development",
|
||||||
"test": "cross-env TZ=Etc/UTC jest",
|
"test": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest",
|
||||||
"test:watch": "cross-env TZ=Etc/UTC jest --watch",
|
"test:watch": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --watch",
|
||||||
"test:updatesnapshot": "cross-env TZ=Etc/UTC jest --updateSnapshot",
|
"test:updatesnapshot": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --updateSnapshot",
|
||||||
"test:debug": "cross-env TZ=Etc/UTC jest --forceExit --detectOpenHandles --verbose",
|
"test:debug": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --forceExit --detectOpenHandles --verbose",
|
||||||
"test-ci": "cross-env TZ=Etc/UTC jest --ci --maxWorkers=100% --coverage",
|
"test-ci": "cross-env TZ=Etc/UTC LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 jest --ci --maxWorkers=100% --coverage",
|
||||||
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
|
"clean": "rm -rf dist node_modules .eslintcache .stylelintcache tsconfig.tsbuildinfo",
|
||||||
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
|
"stats": "cross-env NODE_ENV=production webpack --profile --json > webpack_stats.json",
|
||||||
"mmjstool": "mmjstool",
|
"mmjstool": "mmjstool",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import {createElement, PureComponent} from 'react';
|
import {createElement, PureComponent} from 'react';
|
||||||
|
|
||||||
import ListItem from './item_row_shared';
|
import ListItem from './list_item';
|
||||||
|
|
||||||
const atBottomMargin = 10;
|
const atBottomMargin = 10;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {render, screen} from '@testing-library/react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
jest.mock('./list_item_size_observer', () => {
|
||||||
|
const mockObserve = jest.fn(() => jest.fn());
|
||||||
|
const mockUnobserve = jest.fn();
|
||||||
|
|
||||||
|
return {
|
||||||
|
ListItemSizeObserver: {
|
||||||
|
getInstance: jest.fn(() => ({
|
||||||
|
observe: mockObserve,
|
||||||
|
unobserve: mockUnobserve,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('lodash/debounce', () => {
|
||||||
|
return jest.fn((fn) => {
|
||||||
|
const debouncedFn = (...args: any[]) => fn(...args);
|
||||||
|
debouncedFn.cancel = jest.fn();
|
||||||
|
debouncedFn.flush = jest.fn();
|
||||||
|
return debouncedFn;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
import ListItem from './list_item';
|
||||||
|
|
||||||
|
describe('ListItem', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
item: <div data-testid='test-item'>{'Test Item Content'}</div>,
|
||||||
|
itemId: 'test-item-1',
|
||||||
|
index: 0,
|
||||||
|
height: 100,
|
||||||
|
width: 300,
|
||||||
|
onHeightChange: jest.fn(),
|
||||||
|
onUnmount: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the item content correctly', () => {
|
||||||
|
render(<ListItem {...defaultProps}/>);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('test-item')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Test Item Content')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies correct attributes to the wrapper div', () => {
|
||||||
|
render(<ListItem {...defaultProps}/>);
|
||||||
|
|
||||||
|
const wrapper = screen.getByRole('listitem');
|
||||||
|
expect(wrapper).toHaveClass('item_measurer');
|
||||||
|
expect(wrapper).toHaveAttribute('role', 'listitem');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls onHeightChange on mount with initial height', () => {
|
||||||
|
const mockOnHeightChange = jest.fn();
|
||||||
|
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||||
|
configurable: true,
|
||||||
|
value: 120,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ListItem
|
||||||
|
{...defaultProps}
|
||||||
|
onHeightChange={mockOnHeightChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockOnHeightChange).toHaveBeenCalledWith('test-item-1', 120, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles zero offsetHeight gracefully', () => {
|
||||||
|
const mockOnHeightChange = jest.fn();
|
||||||
|
|
||||||
|
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
|
||||||
|
configurable: true,
|
||||||
|
value: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<ListItem
|
||||||
|
{...defaultProps}
|
||||||
|
onHeightChange={mockOnHeightChange}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockOnHeightChange).toHaveBeenCalledWith('test-item-1', 0, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,7 +5,7 @@ import debounce from 'lodash/debounce';
|
|||||||
import type {ReactNode} from 'react';
|
import type {ReactNode} from 'react';
|
||||||
import React, {memo, useEffect, useRef} from 'react';
|
import React, {memo, useEffect, useRef} from 'react';
|
||||||
|
|
||||||
import {ListItemSizeObserver} from './item_row_size_observer';
|
import {ListItemSizeObserver} from './list_item_size_observer';
|
||||||
|
|
||||||
const RESIZE_DEBOUNCE_TIME = 200; // in ms
|
const RESIZE_DEBOUNCE_TIME = 200; // in ms
|
||||||
|
|
||||||
@@ -50,9 +50,15 @@ const ListItem = (props: Props) => {
|
|||||||
// This effects adds the observer which calls height change callback debounced
|
// This effects adds the observer which calls height change callback debounced
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const debouncedOnHeightChange = debounce((changedHeight: number) => {
|
const debouncedOnHeightChange = debounce((changedHeight: number) => {
|
||||||
|
// Check if component is still mounted as it may have been
|
||||||
|
// unmounted by the time the debounced function is called
|
||||||
|
if (!rowRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// If width of container has changed then scroll bar position will be out of sync
|
// If width of container has changed then scroll bar position will be out of sync
|
||||||
// so we need to force a scroll correction
|
// so we need to force a scroll correction
|
||||||
const forceScrollCorrection = rowRef.current?.offsetWidth !== widthRef.current;
|
const forceScrollCorrection = rowRef.current.offsetWidth !== widthRef.current;
|
||||||
|
|
||||||
heightRef.current = changedHeight;
|
heightRef.current = changedHeight;
|
||||||
|
|
||||||
@@ -76,11 +82,10 @@ const ListItem = (props: Props) => {
|
|||||||
cleanupSizeObserver = listItemSizeObserver.observe(props.itemId, rowRef.current, itemRowSizeObserverCallback);
|
cleanupSizeObserver = listItemSizeObserver.observe(props.itemId, rowRef.current, itemRowSizeObserverCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We remove the observer here from a row
|
|
||||||
return () => {
|
return () => {
|
||||||
if (cleanupSizeObserver) {
|
// We remove the observer here from a row
|
||||||
cleanupSizeObserver();
|
cleanupSizeObserver?.();
|
||||||
}
|
debouncedOnHeightChange?.cancel();
|
||||||
props.onUnmount(props.itemId, indexRef.current);
|
props.onUnmount(props.itemId, indexRef.current);
|
||||||
};
|
};
|
||||||
}, [props.itemId]);
|
}, [props.itemId]);
|
||||||
Ссылка в новой задаче
Block a user