Skip to content

Commit a19d96b

Browse files
Merge pull request #459 from gemini-testing/HERMIONE-782.up_scale_to_fit
feat: add diff mode '3-up scaled to fit'
2 parents 7c3919e + 75624cd commit a19d96b

File tree

14 files changed

+199
-102
lines changed

14 files changed

+199
-102
lines changed

lib/constants/diff-modes.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ module.exports = {
99
title: '3-up scaled',
1010
description: 'scaled images in row'
1111
},
12+
THREE_UP_SCALED_TO_FIT: {
13+
id: '3-up-scaled-to-fit',
14+
title: '3-up scaled to fit',
15+
description: 'scaled to browser height images in row'
16+
},
1217
ONLY_DIFF: {
1318
id: 'only-diff',
1419
title: 'Only diff',

lib/static/components/controls/selects/index.styl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
&.diff-mode
4141
.select__dropdown.ui.dropdown
42-
min-width: 90px
42+
min-width: 95px
4343

4444
&.select_type_group
4545
.select__dropdown.ui.dropdown

lib/static/components/state/screenshot/resized.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ class ResizedScreenshot extends Component {
1717
}
1818

1919
static propTypes = {
20-
noCache: PropTypes.bool,
2120
image: PropTypes.shape({
2221
path: PropTypes.string.isRequired,
2322
size: PropTypes.shape({
@@ -26,6 +25,7 @@ class ResizedScreenshot extends Component {
2625
})
2726
}).isRequired,
2827
diffClusters: PropTypes.array,
28+
overrideWidth: PropTypes.number,
2929
// from withEncodeUri
3030
imageUrl: PropTypes.string.isRequired
3131
}
@@ -53,7 +53,7 @@ class ResizedScreenshot extends Component {
5353
}
5454

5555
render() {
56-
const {imageUrl, image: {size: imgSize}, diffClusters, style} = this.props;
56+
const {imageUrl, image: {size: imgSize}, diffClusters, overrideWidth, style} = this.props;
5757
const cursorStyle = diffClusters ? {cursor: 'pointer'} : {};
5858

5959
if (!imgSize) {
@@ -76,7 +76,7 @@ class ResizedScreenshot extends Component {
7676
<div
7777
onClick={diffClusters && this._handleDiffClick(diffClusters)}
7878
className="image-box__screenshot-container image-box__screenshot-container_fixed-size"
79-
style={{width: imgSize.width, paddingTop: `${paddingTop}%`}}
79+
style={{width: overrideWidth || imgSize.width, paddingTop: `${paddingTop}%`}}
8080
>
8181
<img src={imageUrl} className={imgClassNames} style={{...style, ...cursorStyle}} />
8282
</div>
Lines changed: 111 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use strict';
22

3-
import React, {Component, Fragment} from 'react';
3+
import React, {Fragment, useEffect, useState} from 'react';
44
import {connect} from 'react-redux';
55
import PropTypes from 'prop-types';
66
import classNames from 'classnames';
@@ -9,68 +9,102 @@ import SwipeDiff from './swipe-diff';
99
import SwitchDiff from './switch-diff';
1010
import OnionSkinDiff from './onion-skin-diff';
1111
import diffModes from '../../../../constants/diff-modes';
12+
import {types} from '../../modals';
13+
import useFitImages from './useFitImages';
1214

1315
import './index.styl';
1416

15-
class StateFail extends Component {
16-
static propTypes = {
17-
image: PropTypes.shape({
18-
expectedImg: PropTypes.object.isRequired,
19-
actualImg: PropTypes.object.isRequired,
20-
diffImg: PropTypes.object.isRequired,
21-
diffClusters: PropTypes.array
22-
}).isRequired,
23-
// from store
24-
diffMode: PropTypes.string
25-
}
26-
27-
constructor(props) {
28-
super(props);
29-
30-
this.state = {diffMode: this.props.diffMode};
31-
}
17+
const StateFail = ({image, diffMode: diffModeProp, isScreenshotAccepterOpened}) => {
18+
const [diffMode, setDiffMode] = useState(diffModeProp);
19+
const [fitWidths, {expectedRef, actualRef}] = useFitImages(image, isScreenshotAccepterOpened);
3220

33-
componentWillReceiveProps(nextProps) {
34-
if (this.state.diffMode !== nextProps.diffMode) {
35-
this.setState({diffMode: nextProps.diffMode});
36-
}
37-
}
21+
useEffect(() => {
22+
setDiffMode(diffModeProp);
23+
}, [diffModeProp, setDiffMode]);
3824

39-
_handleDiffModeClick = (diffMode) => {
40-
if (this.state.diffMode === diffMode) {
41-
return;
42-
}
25+
const renderDiffModeItem = (mode) => {
26+
const className = classNames(
27+
'diff-modes__item',
28+
{'diff-modes__active': diffMode === mode.id}
29+
);
4330

44-
this.setState({diffMode});
45-
}
31+
return (
32+
<li
33+
key={mode.id}
34+
title={mode.description}
35+
className={className}
36+
onClick={() => setDiffMode(mode.id)}
37+
>
38+
{mode.title}
39+
</li>
40+
);
41+
};
4642

47-
_renderDiffModeItems() {
48-
const diffModeItems = Object.values(diffModes).map((diffMode) => {
49-
return this._renderDiffModeItem(diffMode);
50-
});
43+
const renderDiffModeItems = () => {
44+
const diffModeItems = Object.values(diffModes).map(renderDiffModeItem);
5145

5246
return (
5347
<ul className="diff-modes">
5448
{diffModeItems}
5549
</ul>
5650
);
57-
}
51+
};
5852

59-
_renderDiffModeItem(diffMode) {
60-
const className = classNames(
61-
'diff-modes__item',
62-
{'diff-modes__active': this.state.diffMode === diffMode.id}
53+
const getLabelKey = () => {
54+
const images = [image.expectedImg, image.actualImg];
55+
const sizes = images.map(image => `${image.size.width}${image.size.height}`);
56+
const key = sizes.join('');
57+
58+
return key;
59+
};
60+
61+
const drawImageBox = (image, {label, diffClusters, width, ref} = {}) => {
62+
const titleText = `${label} (${image.size.width}x${image.size.height})`;
63+
const titleKey = getLabelKey();
64+
65+
return (
66+
<div className="image-box__image" style={{flex: image.size.width}}>
67+
{label && <div key={titleKey} ref={ref} className="image-box__title">{titleText}</div>}
68+
<ResizedScreenshot
69+
image={image}
70+
diffClusters={diffClusters}
71+
overrideWidth={width}
72+
/>
73+
</div>
6374
);
75+
};
6476

65-
return <li key={diffMode.id} title={diffMode.description} className={className} onClick={() => this._handleDiffModeClick(diffMode.id)}>{diffMode.title}</li>;
66-
}
77+
const renderOnlyDiff = () => {
78+
const {diffImg, diffClusters} = image;
79+
80+
return drawImageBox(diffImg, {diffClusters});
81+
};
82+
83+
const drawExpectedAndActual = ({expectedImg, expectedWidth}, {actualImg, actualWidth}) => {
84+
return (
85+
<Fragment>
86+
{drawImageBox(expectedImg, {label: 'Expected', width: expectedWidth, ref: expectedRef})}
87+
{drawImageBox(actualImg, {label: 'Actual', width: actualWidth, ref: actualRef})}
88+
</Fragment>
89+
);
90+
};
91+
92+
const renderThreeImages = (fitWidths = []) => {
93+
const {expectedImg, actualImg, diffImg, diffClusters} = image;
94+
const [expectedWidth, actualWidth, diffWidth] = fitWidths;
95+
96+
return <Fragment>
97+
{drawExpectedAndActual({expectedImg, expectedWidth}, {actualImg, actualWidth})}
98+
{drawImageBox(diffImg, {label: 'Diff', diffClusters, width: diffWidth})}
99+
</Fragment>;
100+
};
67101

68-
_renderImages() {
69-
const {image: {expectedImg, actualImg}} = this.props;
102+
const renderImages = () => {
103+
const {expectedImg, actualImg} = image;
70104

71-
switch (this.state.diffMode) {
105+
switch (diffMode) {
72106
case diffModes.ONLY_DIFF.id:
73-
return this._renderOnlyDiff();
107+
return renderOnlyDiff();
74108

75109
case diffModes.SWITCH.id:
76110
return <SwitchDiff image1={expectedImg} image2={actualImg} />;
@@ -83,60 +117,45 @@ class StateFail extends Component {
83117

84118
case diffModes.THREE_UP_SCALED.id:
85119
return <div className="image-box__scaled">
86-
{this._renderThreeImages()}
120+
{renderThreeImages()}
121+
</div>;
122+
123+
case diffModes.THREE_UP_SCALED_TO_FIT.id:
124+
return <div className="image-box__scaled">
125+
{renderThreeImages(fitWidths)}
87126
</div>;
88127

89128
case diffModes.THREE_UP.id:
90129
default:
91-
return this._renderThreeImages();
130+
return renderThreeImages();
92131
}
93-
}
94-
95-
_renderOnlyDiff() {
96-
const {image: {diffImg, diffClusters}} = this.props;
97-
98-
return this._drawImageBox(diffImg, {diffClusters});
99-
}
100-
101-
_renderThreeImages() {
102-
const {image: {expectedImg, actualImg, diffImg, diffClusters}} = this.props;
103-
104-
return <Fragment>
105-
{this._drawExpectedAndActual(expectedImg, actualImg)}
106-
{this._drawImageBox(diffImg, {label: 'Diff', diffClusters})}
107-
</Fragment>;
108-
}
109-
110-
render() {
111-
return (
112-
<Fragment>
113-
{this._renderDiffModeItems()}
114-
{this._renderImages()}
115-
</Fragment>
116-
);
117-
}
118-
119-
_drawExpectedAndActual(expectedImg, actualImg) {
120-
return (
121-
<Fragment>
122-
{this._drawImageBox(expectedImg, {label: 'Expected'})}
123-
{this._drawImageBox(actualImg, {label: 'Actual'})}
124-
</Fragment>
125-
);
126-
}
132+
};
133+
134+
return (
135+
<Fragment>
136+
{renderDiffModeItems()}
137+
{renderImages()}
138+
</Fragment>
139+
);
140+
};
141+
142+
StateFail.propTypes = {
143+
image: PropTypes.shape({
144+
expectedImg: PropTypes.object.isRequired,
145+
actualImg: PropTypes.object.isRequired,
146+
diffImg: PropTypes.object.isRequired,
147+
diffClusters: PropTypes.array
148+
}).isRequired,
149+
// from store
150+
diffMode: PropTypes.string,
151+
isScreenshotAccepterOpened: PropTypes.bool.isRequired
152+
};
127153

128-
_drawImageBox(image, {label, diffClusters} = {}) {
129-
const text = `${label} (${image.size.width}x${image.size.height})`;
154+
export default connect(
155+
({modals, view}) => {
156+
const diffMode = view.diffMode;
157+
const isScreenshotAccepterOpened = modals.some(modal => modal.id === types.SCREENSHOT_ACCEPTER);
130158

131-
return (
132-
<div className="image-box__image" style={{flex: image.size.width}}>
133-
{label && <div className="image-box__title">{text}</div>}
134-
<ResizedScreenshot image={image} diffClusters={diffClusters}/>
135-
</div>
136-
);
159+
return {diffMode, isScreenshotAccepterOpened};
137160
}
138-
}
139-
140-
export default connect(
141-
({view: {diffMode}}) => ({diffMode})
142161
)(StateFail);

lib/static/components/state/state-fail/index.styl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
border-bottom: 1px solid #ccc
55
padding: 5px 0
66
margin: 10px auto
7-
width: 420px
7+
width: 520px
88

99
&__item
1010
display: inline-block
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import {useMemo} from 'react';
2+
import {clamp} from 'lodash';
3+
import useWindowSize from '../../../hooks/useWindowSize';
4+
import useElementSize from '../../../hooks/useElementSize';
5+
6+
const TITLE_DEFAULT_TOP_OFFSET = 280;
7+
const MIN_SHRINK_RATIO = 1;
8+
const MAX_SHRINK_RATIO = 2;
9+
10+
export default function useFitImages(image, isScreenshotAccepterOpened) {
11+
const {height: windowHeight} = useWindowSize();
12+
const [expectedRef, expectedPos] = useElementSize();
13+
const [actualRef, actualPos] = useElementSize();
14+
15+
const imagesWidth = useMemo(() => {
16+
return [image.expectedImg, image.actualImg, image.diffImg].map(img => img.size.width);
17+
}, [image]);
18+
19+
const imagesHeight = useMemo(() => {
20+
return [image.expectedImg, image.actualImg, image.diffImg].map(img => img.size.height);
21+
}, [image]);
22+
23+
const sectionsWidth = useMemo(() => {
24+
const expectedWidth = expectedPos.width;
25+
const actualWidth = actualPos.width;
26+
const diffWidth = Math.max(expectedWidth, actualWidth);
27+
28+
return [expectedWidth, actualWidth, diffWidth];
29+
}, [expectedPos, actualPos]);
30+
31+
const displayedImagesWidth = useMemo(() => {
32+
return sectionsWidth.map((sectionWidth, i) => Math.min(sectionWidth, imagesWidth[i]));
33+
}, [sectionsWidth, imagesWidth]);
34+
35+
const displayedImagesHeight = useMemo(() => {
36+
return imagesHeight.map((height, i) => height * displayedImagesWidth[i] / imagesWidth[i]);
37+
}, [imagesHeight, imagesWidth, displayedImagesWidth]);
38+
39+
const topOffsets = useMemo(() => {
40+
const expectedRightBorder = expectedPos.left + expectedPos.width;
41+
const actualLeftBorder = actualPos.left;
42+
const imageGap = (actualLeftBorder - expectedRightBorder) / 2;
43+
44+
const expectedTitleOffset = isScreenshotAccepterOpened ? expectedPos.top : TITLE_DEFAULT_TOP_OFFSET;
45+
const actualTitleOffset = isScreenshotAccepterOpened ? actualPos.top : TITLE_DEFAULT_TOP_OFFSET;
46+
47+
const expectedImageOffset = expectedTitleOffset + expectedPos.height + imageGap;
48+
const actualImageOffset = actualTitleOffset + actualPos.height + imageGap;
49+
const diffImageOffset = Math.min(expectedImageOffset, actualImageOffset);
50+
51+
return [expectedImageOffset, actualImageOffset, diffImageOffset];
52+
}, [expectedPos, actualPos, isScreenshotAccepterOpened]);
53+
54+
return useMemo(() => {
55+
const availableHeights = topOffsets.map(offset => windowHeight - offset);
56+
const shrinkCoef = displayedImagesHeight.reduce((acc, height, i) => {
57+
return clamp(height / availableHeights[i], acc, MAX_SHRINK_RATIO);
58+
}, MIN_SHRINK_RATIO);
59+
const fitWidths = displayedImagesWidth.map((width) => width / shrinkCoef);
60+
61+
return [fitWidths, {expectedRef, actualRef}];
62+
}, [topOffsets, displayedImagesWidth, windowHeight]);
63+
}

lib/static/hooks/useElementSize.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {useState, useCallback, useLayoutEffect} from 'react';
22

33
import useEventListener from './useEventListener';
44

5-
export default function useElementSize() {
5+
export default function useElementSize({shouldListenResize = false} = {}) {
66
const [ref, setRef] = useState(null);
77
const [size, setSize] = useState({
88
width: 0,
@@ -20,7 +20,7 @@ export default function useElementSize() {
2020
});
2121
}, [ref && ref.offsetHeight, ref && ref.offsetWidth, ref && ref.offsetLeft, ref && ref.offsetTop]);
2222

23-
useEventListener('resize', handlePosition);
23+
shouldListenResize && useEventListener('resize', handlePosition);
2424

2525
useLayoutEffect(() => {
2626
handlePosition();

0 commit comments

Comments
 (0)