From bd6f9e620352e65bb620b97e78fa61c50120ba0b Mon Sep 17 00:00:00 2001 From: shadowusr Date: Tue, 20 Aug 2024 14:44:22 +0300 Subject: [PATCH 1/2] fix: fix flickering when expanding/collapsing sections --- lib/static/components/details.jsx | 122 +++++++------- lib/static/components/measurement-context.js | 3 + .../components/section/body/description.jsx | 1 - .../components/section/body/history/index.jsx | 11 +- lib/static/components/section/body/index.jsx | 95 ++++++----- .../section/body/meta-info/content.jsx | 59 +++---- .../section/body/meta-info/index.jsx | 1 - .../section/body/page-screenshot.tsx | 1 - .../components/section/section-browser.jsx | 153 +++++++++--------- .../components/section/section-common.jsx | 130 ++++++++------- .../components/section/section-wrapper.jsx | 20 --- lib/static/components/section/utils.js | 10 ++ lib/static/components/state/error-details.jsx | 2 +- lib/static/components/suites.jsx | 23 ++- 14 files changed, 310 insertions(+), 321 deletions(-) create mode 100644 lib/static/components/measurement-context.js delete mode 100644 lib/static/components/section/section-wrapper.jsx create mode 100644 lib/static/components/section/utils.js diff --git a/lib/static/components/details.jsx b/lib/static/components/details.jsx index c9f4bef2f..577697948 100644 --- a/lib/static/components/details.jsx +++ b/lib/static/components/details.jsx @@ -1,89 +1,81 @@ 'use strict'; -import React, {Component} from 'react'; +import React, {useContext, useLayoutEffect, useState} from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import {isEmpty, isFunction} from 'lodash'; -import {Card, Disclosure} from '@gravity-ui/uikit'; +import {Disclosure} from '@gravity-ui/uikit'; +import {MeasurementContext} from './measurement-context'; -export default class Details extends Component { - static propTypes = { - type: PropTypes.oneOf(['text', 'image']), - title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired, - content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired, - extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), - onClick: PropTypes.func, - asHtml: PropTypes.bool - }; - - state = {isOpened: false}; - - handleClick = () => { - this.setState((state, props) => { - const newState = {isOpened: !state.isOpened}; +export default function Details(props) { + const [isOpened, setIsOpened] = useState(false); - if (props.onClick) { - props.onClick(newState); - } - - return newState; - }); - }; + const handleClick = () => { + const newIsOpened = !isOpened; - stopPropagation = (e) => { - e.stopPropagation(); + setIsOpened(newIsOpened); + props.onClick?.(newIsOpened); }; - _getContent() { - const content = this.props.content; + const getContent = () => { + const {content} = props; return isFunction(content) ? content() : content; - } + }; - _renderContent() { - if (!this.state.isOpened) { + const renderContent = () => { + if (!isOpened) { return null; } - const children = this.props.asHtml ? null : this._getContent(); - const extraProps = this.props.asHtml ? {dangerouslySetInnerHTML: {__html: this._getContent()}} : {}; + const children = props.asHtml ? null : getContent(); + const extraProps = props.asHtml ? {dangerouslySetInnerHTML: {__html: getContent()}} : {}; return
{children}
; - } + }; - render() { - const {type, title, content, extendClassNames} = this.props; - const className = classNames( - 'details', - extendClassNames - ); + const {title, content, extendClassNames} = props; + const className = classNames( + 'details', + extendClassNames + ); - return ( - isEmpty(content) && !isFunction(content) ? ( -
- {title} -
- ) : ( - - - {(props, defaultButton) => ( -
-
- {defaultButton} -
- {title} + const {measure} = useContext(MeasurementContext); + useLayoutEffect(() => { + measure?.(); + }, [isOpened]); + + return ( + isEmpty(content) && !isFunction(content) ? ( +
+ {title} +
+ ) : ( + + + {(props, defaultButton) => ( +
+
e.stopPropagation()}> + {defaultButton}
- )} - - {type === 'image' ? this._renderContent() : - - {this._renderContent()} - } - - ) - ); - } + {title} +
+ )} +
+ {renderContent()} +
+ ) + ); } + +Details.propTypes = { + id: PropTypes.string, + ariaControls: PropTypes.arrayOf(PropTypes.string), + title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired, + content: PropTypes.oneOfType([PropTypes.func, PropTypes.string, PropTypes.element, PropTypes.array]).isRequired, + extendClassNames: PropTypes.oneOfType([PropTypes.array, PropTypes.string]), + onClick: PropTypes.func, + asHtml: PropTypes.bool +}; diff --git a/lib/static/components/measurement-context.js b/lib/static/components/measurement-context.js new file mode 100644 index 000000000..b7e420aac --- /dev/null +++ b/lib/static/components/measurement-context.js @@ -0,0 +1,3 @@ +import {createContext} from 'react'; + +export const MeasurementContext = createContext({measure: () => {}}); diff --git a/lib/static/components/section/body/description.jsx b/lib/static/components/section/body/description.jsx index c87385bad..8670dc27c 100644 --- a/lib/static/components/section/body/description.jsx +++ b/lib/static/components/section/body/description.jsx @@ -14,7 +14,6 @@ export default class Description extends Component { render() { return
; diff --git a/lib/static/components/section/body/history/index.jsx b/lib/static/components/section/body/history/index.jsx index 30ea9ae5f..f8fbeee54 100644 --- a/lib/static/components/section/body/history/index.jsx +++ b/lib/static/components/section/body/history/index.jsx @@ -5,7 +5,7 @@ import {isEmpty} from 'lodash'; import Details from '../../../details'; import './index.styl'; -import {List} from '@gravity-ui/uikit'; +import {Card, List} from '@gravity-ui/uikit'; const History = ({history}) => { const renderHistoryItem = (item) => { @@ -21,12 +21,13 @@ const History = ({history}) => { isEmpty(history) ? null :
- -
+ +
+ +
+
} extendClassNames='history' /> diff --git a/lib/static/components/section/body/index.jsx b/lib/static/components/section/body/index.jsx index cfa5fab79..ac29b0cf9 100644 --- a/lib/static/components/section/body/index.jsx +++ b/lib/static/components/section/body/index.jsx @@ -1,4 +1,4 @@ -import React, {Component, Fragment} from 'react'; +import React, {Fragment, useContext, useRef} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import {isNumber} from 'lodash'; @@ -10,38 +10,28 @@ import * as actions from '../../../modules/actions'; import ExtensionPoint from '../../extension-point'; import {RESULT} from '../../../../constants/extension-points'; import {ArrowRotateLeft} from '@gravity-ui/icons'; +import useResizeObserver from '@react-hook/resize-observer'; +import {MeasurementContext} from '../../measurement-context'; -class Body extends Component { - static propTypes = { - browserId: PropTypes.string.isRequired, - browserName: PropTypes.string.isRequired, - testName: PropTypes.string.isRequired, - resultIds: PropTypes.array.isRequired, - // from store - gui: PropTypes.bool.isRequired, - running: PropTypes.bool.isRequired, - retryIndex: PropTypes.number, - actions: PropTypes.object.isRequired - }; - - onRetrySwitcherChange = (index) => { - const {browserId, retryIndex} = this.props; +function Body(props) { + const onRetrySwitcherChange = (index) => { + const {browserId, retryIndex} = props; if (index === retryIndex) { return; } - this.props.actions.changeTestRetry({browserId, retryIndex: index}); + props.actions.changeTestRetry({browserId, retryIndex: index}); }; - onTestRetry = () => { - const {testName, browserName} = this.props; + const onTestRetry = () => { + const {testName, browserName} = props; - this.props.actions.retryTest({testName, browserName}); + props.actions.retryTest({testName, browserName}); }; - _addRetrySwitcher = () => { - const {resultIds, retryIndex} = this.props; + const addRetrySwitcher = () => { + const {resultIds, retryIndex} = props; if (resultIds.length <= 1) { return; @@ -52,14 +42,14 @@ class Body extends Component { ); }; - _addRetryButton = () => { - const {gui, running} = this.props; + const addRetryButton = () => { + const {gui, running} = props; return gui ? ( @@ -71,7 +61,7 @@ class Body extends Component { } isSuiteControl={true} isDisabled={running} - handler={this.onTestRetry} + handler={onTestRetry} dataTestId="test-retry" /> @@ -79,30 +69,47 @@ class Body extends Component { : null; }; - _getActiveResultId = () => { - return this.props.resultIds[this.props.retryIndex]; + const getActiveResultId = () => { + return props.resultIds[props.retryIndex]; }; - render() { - const {testName} = this.props; - const activeResultId = this._getActiveResultId(); - - return ( -
-
-
- {this._addRetrySwitcher()} - {this._addRetryButton()} -
- - - + const {testName} = props; + const activeResultId = getActiveResultId(); + + const {measure} = useContext(MeasurementContext); + const resizeObserverRef = useRef(null); + useResizeObserver(resizeObserverRef, () => { + measure(); + }); + + return ( +
+
+
+ {addRetrySwitcher()} + {addRetryButton()}
+ + +
- ); - } +
+ ); } +Body.propTypes = { + browserId: PropTypes.string.isRequired, + browserName: PropTypes.string.isRequired, + testName: PropTypes.string.isRequired, + resultIds: PropTypes.array.isRequired, + onResize: PropTypes.func, + // from store + gui: PropTypes.bool.isRequired, + running: PropTypes.bool.isRequired, + retryIndex: PropTypes.number, + actions: PropTypes.object.isRequired +}; + export default connect( ({gui, running, view: {retryIndex: viewRetryIndex}, tree}, {browserId}) => { const {retryIndex: browserRetryIndex, lastMatchedRetryIndex} = tree.browsers.stateById[browserId] || {}; diff --git a/lib/static/components/section/body/meta-info/content.jsx b/lib/static/components/section/body/meta-info/content.jsx index cc843208f..77d0291b7 100644 --- a/lib/static/components/section/body/meta-info/content.jsx +++ b/lib/static/components/section/body/meta-info/content.jsx @@ -5,6 +5,7 @@ import {DefinitionList} from '@gravity-ui/components'; import PropTypes from 'prop-types'; import {map, mapValues, isObject, omitBy, isEmpty} from 'lodash'; import {isUrl, getUrlWithBase, getRelativeUrl} from '../../../../../common-utils'; +import {Card} from '@gravity-ui/uikit'; const serializeMetaValues = (metaInfo) => mapValues(metaInfo, (v) => isObject(v) ? JSON.stringify(v) : v); @@ -25,34 +26,36 @@ const resolveUrl = (baseUrl, value) => { }; const metaToElements = (metaInfo, metaInfoBaseUrls) => { - return { - let url = value.url; - value = value.content; - - if (isUrl(value)) { - url = value; - } else if (metaInfoBaseUrls[key] && metaInfoBaseUrls[key] !== 'auto') { - const baseUrl = metaInfoBaseUrls[key]; - const link = isUrl(baseUrl) ? resolveUrl(baseUrl, value) : path.join(baseUrl, value); - url = link; - } else if (typeof value === 'boolean') { - value = value.toString(); - } - - if (url) { - value = - {value} - ; - } - - return { - name: key, - content:
{value}
, - copyText: url || value - }; - }) - }/>; + return + { + let url = value.url; + value = value.content; + + if (isUrl(value)) { + url = value; + } else if (metaInfoBaseUrls[key] && metaInfoBaseUrls[key] !== 'auto') { + const baseUrl = metaInfoBaseUrls[key]; + const link = isUrl(baseUrl) ? resolveUrl(baseUrl, value) : path.join(baseUrl, value); + url = link; + } else if (typeof value === 'boolean') { + value = value.toString(); + } + + if (url) { + value = + {value} + ; + } + + return { + name: key, + content:
{value}
, + copyText: url || value + }; + }) + }/> +
; }; class MetaInfoContent extends Component { diff --git a/lib/static/components/section/body/meta-info/index.jsx b/lib/static/components/section/body/meta-info/index.jsx index b5802d460..39e4fd2c6 100644 --- a/lib/static/components/section/body/meta-info/index.jsx +++ b/lib/static/components/section/body/meta-info/index.jsx @@ -20,7 +20,6 @@ class MetaInfo extends Component { const {resultId} = this.props; return
} onClick={this.onToggleMetaInfo} diff --git a/lib/static/components/section/body/page-screenshot.tsx b/lib/static/components/section/body/page-screenshot.tsx index 393e4db0a..374620382 100644 --- a/lib/static/components/section/body/page-screenshot.tsx +++ b/lib/static/components/section/body/page-screenshot.tsx @@ -10,7 +10,6 @@ interface PageScreenshotProps { export class PageScreenshot extends Component { render(): JSX.Element { return
} />; diff --git a/lib/static/components/section/section-browser.jsx b/lib/static/components/section/section-browser.jsx index e047ad62a..146efb603 100644 --- a/lib/static/components/section/section-browser.jsx +++ b/lib/static/components/section/section-browser.jsx @@ -1,109 +1,112 @@ -import React, {Component, Fragment} from 'react'; +import React, {Fragment, useContext, useLayoutEffect} from 'react'; import {last, isEmpty} from 'lodash'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import Parser from 'html-react-parser'; import PropTypes from 'prop-types'; import * as actions from '../../modules/actions'; -import SectionWrapper from './section-wrapper'; import BrowserTitle from './title/browser'; import BrowserSkippedTitle from './title/browser-skipped'; import Body from './body'; import {isSkippedStatus} from '../../../common-utils'; +import {sectionStatusResolver} from './utils'; +import {MeasurementContext} from '../measurement-context'; -class SectionBrowser extends Component { - static propTypes = { - browserId: PropTypes.string.isRequired, - // from store - browser: PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - resultIds: PropTypes.arrayOf(PropTypes.string).isRequired, - parentId: PropTypes.string.isRequired - }).isRequired, - lastResult: PropTypes.shape({ - id: PropTypes.string.isRequired, - status: PropTypes.string.isRequired, - error: PropTypes.object, - imageIds: PropTypes.arrayOf(PropTypes.string).isRequired, - skipReason: PropTypes.string, - history: PropTypes.arrayOf(PropTypes.string) - }).isRequired, - shouldBeShown: PropTypes.bool.isRequired, - shouldBeOpened: PropTypes.bool.isRequired, - // from SectionCommonWrapper - sectionStatusResolver: PropTypes.func.isRequired, - actions: PropTypes.object.isRequired - }; - - onToggleSection = () => { - const {browserId, shouldBeOpened} = this.props; +function SectionBrowser(props) { + const onToggleSection = () => { + const {browserId, shouldBeOpened} = props; - this.props.actions.toggleBrowserSection({browserId, shouldBeOpened: !shouldBeOpened}); + props.actions.toggleBrowserSection({browserId, shouldBeOpened: !shouldBeOpened}); }; - _generateSkippedTitle(skipReason) { + const generateSkippedTitle = (skipReason) => { return ( - [skipped] {this.props.browser.name} + [skipped] {props.browser.name} {skipReason && ', reason: '} {skipReason && Parser(skipReason)} ); - } + }; - render() { - if (!this.props.shouldBeShown) { - return null; - } + const {browser, shouldBeOpened, shouldBeShown, lastResult} = props; + + const {measure} = useContext(MeasurementContext); + + useLayoutEffect(() => { + measure?.(); + }, [shouldBeShown, shouldBeOpened]); + + if (!shouldBeShown) { + return null; + } - const {browser, shouldBeOpened, lastResult} = this.props; + const isSkippedLastResult = isSkippedStatus(lastResult.status); + const hasRetries = browser.resultIds.length > 1; - const isSkippedLastResult = isSkippedStatus(lastResult.status); - const hasRetries = browser.resultIds.length > 1; + const title = isSkippedLastResult + ? generateSkippedTitle(lastResult.skipReason) + : browser.name; - const title = isSkippedLastResult - ? this._generateSkippedTitle(lastResult.skipReason) - : browser.name; + // Detect executed test but failed and skipped + const isExecutedResult = hasRetries || !isEmpty(lastResult.history) || lastResult.error || lastResult.imageIds.length > 0; + const isSkipped = isSkippedLastResult && !isExecutedResult; - // Detect executed test but failed and skipped - const isExecutedResult = hasRetries || !isEmpty(lastResult.history) || lastResult.error || lastResult.imageIds.length > 0; - const isSkipped = isSkippedLastResult && !isExecutedResult; + const body = isSkipped || !shouldBeOpened + ? null + : ( +
+ ); - const body = isSkipped || !shouldBeOpened - ? null - : ( - + : ( + + - ); - - const section = isSkipped - ? - : ( - - - {body} - - ); - - return ( -
- {section} -
+ {body} +
); - } + + return ( +
+ {section} +
+ ); } +SectionBrowser.propTypes = { + browserId: PropTypes.string.isRequired, + // from store + browser: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + resultIds: PropTypes.arrayOf(PropTypes.string).isRequired, + parentId: PropTypes.string.isRequired + }).isRequired, + lastResult: PropTypes.shape({ + id: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + error: PropTypes.object, + imageIds: PropTypes.arrayOf(PropTypes.string).isRequired, + skipReason: PropTypes.string, + history: PropTypes.arrayOf(PropTypes.string) + }).isRequired, + shouldBeShown: PropTypes.bool.isRequired, + shouldBeOpened: PropTypes.bool.isRequired, + actions: PropTypes.object.isRequired +}; + export default connect( ({tree}, {browserId}) => { const browser = tree.browsers.byId[browserId]; @@ -113,4 +116,4 @@ export default connect( return {browser, lastResult, shouldBeShown, shouldBeOpened}; }, (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(SectionWrapper(SectionBrowser)); +)(SectionBrowser); diff --git a/lib/static/components/section/section-common.jsx b/lib/static/components/section/section-common.jsx index 654341903..918496351 100644 --- a/lib/static/components/section/section-common.jsx +++ b/lib/static/components/section/section-common.jsx @@ -1,88 +1,86 @@ -import React, {Component} from 'react'; +import React, {useContext, useLayoutEffect} from 'react'; import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; import PropTypes from 'prop-types'; import * as actions from '../../modules/actions'; -import SectionWrapper from './section-wrapper'; import SectionBrowser from './section-browser'; import Title from './title/simple'; +import {sectionStatusResolver} from './utils'; +import {MeasurementContext} from '../measurement-context'; -class SectionCommon extends Component { - static propTypes = { - suiteId: PropTypes.string.isRequired, - sectionRoot: PropTypes.bool.isRequired, - // from store - suiteName: PropTypes.string.isRequired, - suiteStatus: PropTypes.string.isRequired, - suiteChildIds: PropTypes.array, - suiteBrowserIds: PropTypes.array, - shouldBeShown: PropTypes.bool.isRequired, - shouldBeOpened: PropTypes.bool.isRequired, - // from SectionCommonWrapper - sectionStatusResolver: PropTypes.func.isRequired, - actions: PropTypes.object.isRequired - }; - - onToggleSection = () => { - const {suiteId, shouldBeOpened} = this.props; - - this.props.actions.toggleSuiteSection({suiteId, shouldBeOpened: !shouldBeOpened}); - }; +function SectionCommon(props) { + const { + suiteId, + suiteName, + suiteStatus, + suiteChildIds, + suiteBrowserIds, + sectionRoot, + shouldBeOpened, + shouldBeShown + } = props; - _drawSection() { - const { - suiteId, - suiteName, - suiteStatus, - suiteChildIds, - suiteBrowserIds, - sectionRoot, - sectionStatusResolver, - shouldBeOpened - } = this.props; + const {measure} = useContext(MeasurementContext); - if (!shouldBeOpened) { - return ( -
- - </div> - ); - } + useLayoutEffect(() => { + measure?.(); + }, [shouldBeShown, shouldBeOpened]); - const childSuitesTmpl = suiteChildIds && suiteChildIds.map((suiteId) => { - // eslint-disable-next-line no-use-before-define - return <SectionCommonWrapper - key={suiteId} - suiteId={suiteId} - sectionRoot={false} - />; - }); + if (!shouldBeShown) { + return null; + } - const browsersTmpl = suiteBrowserIds && suiteBrowserIds.map((browserId) => { - return <SectionBrowser key={browserId} browserId={browserId} />; - }); + const onToggleSection = () => { + const {suiteId, shouldBeOpened} = props; + props.actions.toggleSuiteSection({suiteId, shouldBeOpened: !shouldBeOpened}); + }; + if (!shouldBeOpened) { return ( <div className={sectionStatusResolver({status: suiteStatus, shouldBeOpened, sectionRoot})}> - <Title name={suiteName} suiteId={suiteId} handler={this.onToggleSection} /> - <div className="section__body"> - {childSuitesTmpl} - {browsersTmpl} - </div> + <Title name={suiteName} suiteId={suiteId} handler={onToggleSection} /> </div> ); } - render() { - if (!this.props.shouldBeShown) { - return null; - } + const childSuitesTmpl = suiteChildIds && suiteChildIds.map((suiteId) => { + // eslint-disable-next-line no-use-before-define + return <SectionCommonConnected + key={suiteId} + suiteId={suiteId} + sectionRoot={false} + />; + }); - return this._drawSection(); - } + const browsersTmpl = suiteBrowserIds && suiteBrowserIds.map((browserId) => { + return <SectionBrowser key={browserId} browserId={browserId} />; + }); + + return ( + <div className={sectionStatusResolver({status: suiteStatus, shouldBeOpened, sectionRoot})}> + <Title name={suiteName} suiteId={suiteId} handler={onToggleSection} /> + <div className="section__body"> + {childSuitesTmpl} + {browsersTmpl} + </div> + </div> + ); } -const SectionCommonWrapper = connect( +SectionCommon.propTypes = { + suiteId: PropTypes.string.isRequired, + sectionRoot: PropTypes.bool.isRequired, + // from store + suiteName: PropTypes.string.isRequired, + suiteStatus: PropTypes.string.isRequired, + suiteChildIds: PropTypes.array, + suiteBrowserIds: PropTypes.array, + shouldBeShown: PropTypes.bool.isRequired, + shouldBeOpened: PropTypes.bool.isRequired, + actions: PropTypes.object.isRequired +}; + +const SectionCommonConnected = connect( ({tree}, {suiteId}) => { const suite = tree.suites.byId[suiteId]; const {shouldBeOpened, shouldBeShown} = tree.suites.stateById[suiteId]; @@ -97,6 +95,6 @@ const SectionCommonWrapper = connect( }; }, (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(SectionWrapper(SectionCommon)); +)(SectionCommon); -export default SectionCommonWrapper; +export default SectionCommonConnected; diff --git a/lib/static/components/section/section-wrapper.jsx b/lib/static/components/section/section-wrapper.jsx deleted file mode 100644 index 05737b6a4..000000000 --- a/lib/static/components/section/section-wrapper.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React, {Component} from 'react'; -import classNames from 'classnames'; - -export default (SectionComponent) => class WrappedComponent extends Component { - sectionStatusResolver({status, shouldBeOpened, sectionRoot}) { - const baseClasses = ['section', {'section_collapsed': !shouldBeOpened}]; - - return classNames(baseClasses, { - [`section_status_${status}`]: status, - 'section_root': sectionRoot - }); - } - - render() { - return <SectionComponent - {...this.props} - sectionStatusResolver={this.sectionStatusResolver} - />; - } -}; diff --git a/lib/static/components/section/utils.js b/lib/static/components/section/utils.js new file mode 100644 index 000000000..b49e5252d --- /dev/null +++ b/lib/static/components/section/utils.js @@ -0,0 +1,10 @@ +import classNames from 'classnames'; + +export const sectionStatusResolver = ({status, shouldBeOpened, sectionRoot}) => { + const baseClasses = ['section', {'section_collapsed': !shouldBeOpened}]; + + return classNames(baseClasses, { + [`section_status_${status}`]: status, + 'section_root': sectionRoot + }); +}; diff --git a/lib/static/components/state/error-details.jsx b/lib/static/components/state/error-details.jsx index 4a2259833..f5de48cd4 100644 --- a/lib/static/components/state/error-details.jsx +++ b/lib/static/components/state/error-details.jsx @@ -13,6 +13,6 @@ export default class ErrorDetails extends Component { const {title, filePath} = this.props.errorDetails; const content = <div className="toggle-open__item"><a href={filePath} target="_blank" rel="noreferrer">{title}</a></div>; - return <Details type='text' title='Error details' content={content}/>; + return <Details title='Error details' content={content}/>; } } diff --git a/lib/static/components/suites.jsx b/lib/static/components/suites.jsx index ff9197514..d8e2106b8 100644 --- a/lib/static/components/suites.jsx +++ b/lib/static/components/suites.jsx @@ -1,31 +1,24 @@ -import React, {useRef} from 'react'; +import React, {forwardRef} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; import {isEmpty, flatMap, find, once, throttle} from 'lodash'; import {List, AutoSizer, CellMeasurer, CellMeasurerCache, WindowScroller} from 'react-virtualized'; -import useResizeObserver from '@react-hook/resize-observer'; import {bindActionCreators} from 'redux'; import * as actions from '../modules/actions'; import SectionCommon from './section/section-common'; import {ViewMode} from '../../constants/view-modes'; import {getVisibleRootSuiteIds} from '../modules/selectors/tree'; +import {MeasurementContext} from './measurement-context'; -function VirtualizedRow(props) { - const resizeObserverRef = useRef(null); - - useResizeObserver(resizeObserverRef, props.onResize); - - return <div ref={props.onInit} style={props.style} className="virtualized__row"> - <div ref={resizeObserverRef}> - <SectionCommon {...props.sectionProps} /> - </div> +const VirtualizedRow = forwardRef(function VirtualizedRow(props, ref) { + return <div ref={ref} style={props.style} className="virtualized__row"> + <SectionCommon {...props.sectionProps} /> </div>; -} +}); VirtualizedRow.propTypes = { onInit: PropTypes.func, - onResize: PropTypes.func, style: PropTypes.object, sectionProps: PropTypes.object }; @@ -54,7 +47,9 @@ function Suites(props) { rowIndex={index} > {({measure, registerChild}) => { - return <VirtualizedRow onInit={registerChild} onResize={measure} key={key} style={style} sectionProps={sectionProps} />; + return <MeasurementContext.Provider value={{measure}}> + <VirtualizedRow ref={registerChild} key={key} style={style} sectionProps={sectionProps} /> + </MeasurementContext.Provider>; }} </CellMeasurer> ); From f0b22fd512571dd8fadab4457b72b8234987a7a2 Mon Sep 17 00:00:00 2001 From: shadowusr <nikhakerlab@gmail.com> Date: Tue, 20 Aug 2024 14:53:22 +0300 Subject: [PATCH 2/2] fix: fix unit tests --- lib/static/components/details.jsx | 2 +- .../lib/static/components/section/body/index.jsx | 12 +++++++++++- test/unit/lib/static/components/suites.jsx | 8 -------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/static/components/details.jsx b/lib/static/components/details.jsx index 577697948..86e3a121b 100644 --- a/lib/static/components/details.jsx +++ b/lib/static/components/details.jsx @@ -14,7 +14,7 @@ export default function Details(props) { const newIsOpened = !isOpened; setIsOpened(newIsOpened); - props.onClick?.(newIsOpened); + props.onClick?.({isOpened: newIsOpened}); }; const getContent = () => { diff --git a/test/unit/lib/static/components/section/body/index.jsx b/test/unit/lib/static/components/section/body/index.jsx index ef6f6b004..400b7b74d 100644 --- a/test/unit/lib/static/components/section/body/index.jsx +++ b/test/unit/lib/static/components/section/body/index.jsx @@ -9,6 +9,7 @@ import userEvent from '@testing-library/user-event'; describe('<Body />', () => { const sandbox = sinon.sandbox.create(); let Body, Result, RetrySwitcher, actionsStub; + const originalResizeObserver = window.ResizeObserver; const mkBodyComponent = (props = {}, initialState = {}) => { props = defaults(props, { @@ -35,9 +36,18 @@ describe('<Body />', () => { './result': {default: Result}, '../../retry-switcher': {default: RetrySwitcher} }).default; + + window.ResizeObserver = sinon.stub(); + window.ResizeObserver.prototype.observe = sinon.stub(); + window.ResizeObserver.prototype.unobserve = sinon.stub(); + window.ResizeObserver.prototype.disconnect = sinon.stub(); }); - afterEach(() => sandbox.restore()); + afterEach(() => { + window.ResizeObserver = originalResizeObserver; + + sandbox.restore(); + }); describe('"Retry" button', () => { it('should render if "gui" is running', () => { diff --git a/test/unit/lib/static/components/suites.jsx b/test/unit/lib/static/components/suites.jsx index f14b14293..6567997dc 100644 --- a/test/unit/lib/static/components/suites.jsx +++ b/test/unit/lib/static/components/suites.jsx @@ -11,7 +11,6 @@ describe('<Suites/>', () => { let Suites, getVisibleRootSuiteIds; const originalOffsetHeight = Object.getOwnPropertyDescriptor(global.HTMLElement.prototype, 'offsetHeight'); const originalOffsetWidth = Object.getOwnPropertyDescriptor(global.HTMLElement.prototype, 'offsetWidth'); - const originalResizeObserver = window.ResizeObserver; const mkSuitesComponent = (initialState = {}) => { initialState = defaultsDeep(initialState, { @@ -27,11 +26,6 @@ describe('<Suites/>', () => { Object.defineProperty(global.HTMLElement.prototype, 'offsetHeight', {configurable: true, value: 50}); Object.defineProperty(global.HTMLElement.prototype, 'offsetWidth', {configurable: true, value: 50}); - window.ResizeObserver = sinon.stub(); - window.ResizeObserver.prototype.observe = sinon.stub(); - window.ResizeObserver.prototype.unobserve = sinon.stub(); - window.ResizeObserver.prototype.disconnect = sinon.stub(); - getVisibleRootSuiteIds = sinon.stub().returns([]); Suites = proxyquire('lib/static/components/suites', { @@ -43,8 +37,6 @@ describe('<Suites/>', () => { Object.defineProperty(global.HTMLElement.prototype, 'offsetHeight', originalOffsetHeight); Object.defineProperty(global.HTMLElement.prototype, 'offsetWidth', originalOffsetWidth); - window.ResizeObserver = originalResizeObserver; - sandbox.restore(); });