Skip to content

Commit

Permalink
fix: display common page screenshot in pwt report when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowusr committed Sep 27, 2023
1 parent ce509b6 commit 232ba8c
Show file tree
Hide file tree
Showing 14 changed files with 137 additions and 58 deletions.
2 changes: 1 addition & 1 deletion lib/common-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export const hasDiff = (assertViewResults: AssertViewResult[]): boolean => {
return assertViewResults.some((result) => isImageDiffError(result as {name?: string}));
};

export const isBase64Image = (image: ImageData | ImageBase64 | undefined): image is ImageBase64 => {
export const isBase64Image = (image: ImageData | ImageBase64 | null | undefined): image is ImageBase64 => {
return Boolean((image as ImageBase64 | undefined)?.base64);
};

Expand Down
48 changes: 33 additions & 15 deletions lib/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ImageInfoFail,
ImageInfoFull,
ImagesSaver,
ImageSize
ImageInfoPageSuccess
} from './types';
import {ERROR, FAIL, PluginEvents, SUCCESS, TestStatus, UPDATED} from './constants';
import {
Expand Down Expand Up @@ -66,19 +66,30 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
return _.get(_.find(assertViewResults, {stateName}), 'refImg');
}

static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | ImageData | undefined {
return testResult.error?.screenshot;
static getScreenshot(testResult: ReporterTestResultPlain): ImageBase64 | ImageData | null | undefined {
return testResult.screenshot;
}

getImagesFor(testResult: ReporterTestResultPlain, assertViewStatus: TestStatus, stateName?: string): ImageInfo | undefined {
const refImg = ImageHandler.getRefImg(testResult.assertViewResults, stateName);
const currImg = ImageHandler.getCurrImg(testResult.assertViewResults, stateName);
const errImg = ImageHandler.getScreenshot(testResult);

const pageImg = ImageHandler.getScreenshot(testResult);

const {path: refPath} = this._getExpectedPath(testResult, stateName);
const currPath = utils.getCurrentPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});
const diffPath = utils.getDiffPath({attempt: testResult.attempt, browserId: testResult.browserId, imageDir: testResult.imageDir, stateName});

// Handling whole page common screenshots
if (!stateName && pageImg) {
return {
actualImg: {
path: this._getImgFromStorage(currPath),
size: pageImg.size
}
};
}

if ((assertViewStatus === SUCCESS || assertViewStatus === UPDATED) && refImg) {
const result: ImageInfo = {
expectedImg: {path: this._getImgFromStorage(refPath), size: refImg.size}
Expand Down Expand Up @@ -110,11 +121,11 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
};
}

if (assertViewStatus === ERROR) {
if (assertViewStatus === ERROR && currImg) {
return {
actualImg: {
path: testResult.state?.name ? this._getImgFromStorage(currPath) : '',
size: (currImg?.size || errImg?.size) as ImageSize
path: this._getImgFromStorage(currPath),
size: currImg.size
}
};
}
Expand Down Expand Up @@ -146,14 +157,21 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
) as ImageInfoFull;
}) ?? [];

// common screenshot on test fail
// Common page screenshot
if (ImageHandler.getScreenshot(testResult)) {
const errorImage = _.extend(
{status: ERROR, error: getError(testResult.error)},
this.getImagesFor(testResult, ERROR)
) as ImageInfoError;
const error = getError(testResult.error);

imagesInfo.push(errorImage);
if (!_.isEmpty(error)) {
imagesInfo.push(_.extend(
{status: ERROR, error},
this.getImagesFor(testResult, ERROR)
) as ImageInfoError);
} else {
imagesInfo.push(_.extend(
{status: SUCCESS},
this.getImagesFor(testResult, SUCCESS)
) as ImageInfoPageSuccess);
}
}

return imagesInfo;
Expand Down Expand Up @@ -201,7 +219,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
}));

if (ImageHandler.getScreenshot(testResult)) {
await this._saveErrorScreenshot(testResult);
await this._savePageScreenshot(testResult);
}

await this.emitAsync(PluginEvents.TEST_SCREENSHOTS_SAVED, {
Expand Down Expand Up @@ -303,7 +321,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter {
cacheDiffImages.set(hash, destPath);
}

private async _saveErrorScreenshot(testResult: ReporterTestResultPlain): Promise<void> {
private async _savePageScreenshot(testResult: ReporterTestResultPlain): Promise<void> {
const screenshot = ImageHandler.getScreenshot(testResult);
if (!(screenshot as ImageBase64)?.base64 && !(screenshot as ImageData)?.path) {
logger.warn('Cannot save screenshot on reject');
Expand Down
2 changes: 1 addition & 1 deletion lib/image-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@ export class SqliteImageStore implements ImageStore {
}, suitePathString, browserName);

const imagesInfo: ImageInfoFull[] = imagesInfoResult && JSON.parse(imagesInfoResult[DB_COLUMNS.IMAGES_INFO as keyof Pick<LabeledSuitesRow, 'imagesInfo'>]) || [];
return imagesInfo.find(info => info.stateName === stateName);
return imagesInfo.find(info => (info as {stateName?: string}).stateName === stateName);
}
}
9 changes: 9 additions & 0 deletions lib/static/components/prop-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import PropTypes from 'prop-types';

export const ImageData = PropTypes.shape({
path: PropTypes.string.isRequired,
size: PropTypes.shape({
height: PropTypes.number.isRequired,
width: PropTypes.number.isRequired
}).isRequired
});
18 changes: 18 additions & 0 deletions lib/static/components/section/body/page-screenshot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, {Component} from 'react';
import Details from '../../details';
import ResizedScreenshot from '../../state/screenshot/resized';
import {ImageData} from '../../../../types';

interface PageScreenshotProps {
image: ImageData;
}

export class PageScreenshot extends Component<PageScreenshotProps> {
render(): JSX.Element {
return <Details
title="Page screenshot"
content={(): JSX.Element => <ResizedScreenshot image={this.props.image}/>}
extendClassNames="details_type_image"
/>;
}
}
28 changes: 21 additions & 7 deletions lib/static/components/section/body/result.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {pick} from 'lodash';
import React, {Component, Fragment} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
Expand All @@ -7,6 +8,8 @@ import Description from './description';
import Tabs from './tabs';
import ExtensionPoint from '../../extension-point';
import {RESULT_META} from '../../../../constants/extension-points';
import {PageScreenshot} from './page-screenshot';
import * as projectPropTypes from '../../prop-types';

class Result extends Component {
static propTypes = {
Expand All @@ -17,25 +20,36 @@ class Result extends Component {
status: PropTypes.string.isRequired,
imageIds: PropTypes.array.isRequired,
description: PropTypes.string
}).isRequired
}).isRequired,
pageScreenshot: PropTypes.shape({
actualImg: projectPropTypes.ImageData.isRequired
})
};

render() {
const {result, resultId, testName} = this.props;
const {result, resultId, testName, pageScreenshot} = this.props;

return (
<Fragment>
<ExtensionPoint name={RESULT_META} result={result} testName={testName}>
<MetaInfo resultId={resultId} />
<History resultId={resultId} />
<MetaInfo resultId={resultId}/>
<History resultId={resultId}/>
</ExtensionPoint>
{result.description && <Description content={result.description} />}
<Tabs result={result} />
{result.description && <Description content={result.description}/>}
<Tabs result={result}/>
{pageScreenshot && <hr className="tab__separator"/>}
{pageScreenshot && <PageScreenshot image={pageScreenshot.actualImg}/>}
</Fragment>
);
}
}

export default connect(
({tree}, {resultId}) => ({result: tree.results.byId[resultId]})
({tree}, {resultId}) => {
const result = tree.results.byId[resultId];
const images = Object.values(pick(tree.images.byId, result.imageIds));
const pageScreenshot = images.find(image => !image.stateName && image.actualImg);

return {result, pageScreenshot};
}
)(Result);
28 changes: 22 additions & 6 deletions lib/static/components/section/body/tabs.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React, {Component} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {isEmpty} from 'lodash';
import State from '../../state';
import {isSuccessStatus, isErrorStatus} from '../../../../common-utils';
import {isSuccessStatus, isErrorStatus, isSkippedStatus} from '../../../../common-utils';

export default class Tabs extends Component {
class Tabs extends Component {
static propTypes = {
result: PropTypes.shape({
id: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
imageIds: PropTypes.array.isRequired,
multipleTabs: PropTypes.bool.isRequired,
screenshot: PropTypes.bool.isRequired
screenshot: PropTypes.bool.isRequired,
error: PropTypes.object
}).isRequired
};

Expand All @@ -38,9 +40,14 @@ export default class Tabs extends Component {
const errorTabId = `${result.id}_error`;

if (isEmpty(result.imageIds)) {
return isSuccessStatus(result.status)
? null
: this._drawTab({key: errorTabId});
if (isSuccessStatus(result.status)) {
return null;
}
if (isSkippedStatus(result.status) && isEmpty(result.error)) {
return null;
}

return this._drawTab({key: errorTabId});
}

const tabs = result.imageIds.map((imageId) => this._drawTab({key: imageId, imageId}));
Expand All @@ -50,3 +57,12 @@ export default class Tabs extends Component {
: tabs;
}
}

export default connect(
({tree}, {result}) => {
const filteredResult = {...result};
filteredResult.imageIds = filteredResult.imageIds.filter(imageId => tree.images.byId[imageId].stateName);

return {result: filteredResult};
}
)(Tabs);
17 changes: 3 additions & 14 deletions lib/static/components/state/state-error.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,13 @@ class StateError extends Component {
_drawImage() {
const {image, error} = this.props;

if (!image.actualImg) {
return null;
if (image.actualImg && isNoRefImageError(error)) {
return <ResizedScreenshot image={image.actualImg} />;
}

return isNoRefImageError(error)
? <ResizedScreenshot image={image.actualImg} />
: <Details
title="Page screenshot"
content={() => <ResizedScreenshot image={image.actualImg} />}
extendClassNames="details_type_image"
onClick={this.onTogglePageScreenshot}
/>;
return null;
}

onTogglePageScreenshot = () => {
this.props.actions.togglePageScreenshot();
};

_errorToElements(error) {
return map(error, (value, key) => {
if (!value) {
Expand Down
4 changes: 2 additions & 2 deletions lib/test-adapter/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TestStatus} from '../constants';
import {AssertViewResult, ErrorDetails, ImageBase64, ImageInfoFull, TestError} from '../types';
import {AssertViewResult, ErrorDetails, ImageBase64, ImageData, ImageInfoFull, TestError} from '../types';

export * from './hermione';

Expand All @@ -20,7 +20,7 @@ export interface ReporterTestResult {
readonly isUpdated?: boolean;
readonly meta: Record<string, unknown>;
readonly multipleTabs: boolean;
readonly screenshot: ImageBase64 | undefined;
readonly screenshot: ImageBase64 | ImageData | null | undefined;
readonly sessionId: string;
readonly skipReason?: string;
readonly state: { name: string };
Expand Down
13 changes: 8 additions & 5 deletions lib/test-adapter/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {FAIL, TestStatus, PWT_TITLE_DELIMITER} from '../constants';
import {
AssertViewResult,
ErrorDetails,
ImageBase64,
ImageData,
ImageInfoFull,
ImageSize,
Expand All @@ -36,7 +35,7 @@ export enum ImageTitleEnding {
Previous = '-previous.png'
}

const ANY_IMAGE_ENDING_REGEXP = new RegExp(Object.values(ImageTitleEnding).join('|'));
const ANY_IMAGE_ENDING_REGEXP = new RegExp(Object.values(ImageTitleEnding).map(ending => `${ending}$`).join('|'));

const getStatus = (result: PlaywrightTestResult): TestStatus => {
if (result.status === PwtTestStatus.PASSED) {
Expand Down Expand Up @@ -205,8 +204,10 @@ export class PlaywrightTestAdapter implements ReporterTestResult {
return true;
}

get screenshot(): ImageBase64 | undefined {
return undefined;
get screenshot(): ImageData | null {
const pageScreenshot = this._testResult.attachments.find(a => a.contentType === 'image/png' && a.name === 'screenshot');

return getImageData(pageScreenshot);
}

get sessionId(): string {
Expand Down Expand Up @@ -245,7 +246,9 @@ export class PlaywrightTestAdapter implements ReporterTestResult {
}

private get _attachmentsByState(): Record<string, PlaywrightAttachment[]> {
const imageAttachments = this._testResult.attachments.filter(a => a.contentType === 'image/png');
// Filtering out only images. Page screenshots on reject are named "screenshot", we don't want them in state either.
const imageAttachments = this._testResult.attachments.filter(
a => a.contentType === 'image/png' && ANY_IMAGE_ENDING_REGEXP.test(a.name));

return _.groupBy(imageAttachments, a => a.name.replace(ANY_IMAGE_ENDING_REGEXP, ''));
}
Expand Down
3 changes: 2 additions & 1 deletion lib/tests-tree-builder/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ export class BaseTestsTreeBuilder {
const browserId = this._buildId(suiteId, browserName);
const testResultId = this._buildId(browserId, attempt.toString());
const imageIds = imagesInfo
.map((image: ImageInfoFull, i: number) => this._buildId(testResultId, image.stateName || `${image.status}_${i}`));
.map((image: ImageInfoFull, i: number) =>
this._buildId(testResultId, (image as {stateName?: string}).stateName || `${image.status}_${i}`));

this._addSuites(testPath, browserId);
this._addBrowser({id: browserId, parentId: suiteId, name: browserName, version: browserVersion}, testResultId, attempt);
Expand Down
10 changes: 8 additions & 2 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ export interface ImageInfoSuccess {
actualImg?: ImageData;
}

export interface ImageInfoPageSuccess {
status: TestStatus.SUCCESS;
actualImg: ImageData;
}

export interface ImageInfoError {
status: TestStatus.ERROR;
error?: {message: string; stack: string;}
Expand All @@ -84,12 +89,13 @@ export interface ImageInfoError {
actualImg: ImageData;
}

export type ImageInfoFull = ImageInfoFail | ImageInfoSuccess | ImageInfoError;
export type ImageInfoFull = ImageInfoFail | ImageInfoSuccess | ImageInfoError | ImageInfoPageSuccess;

export type ImageInfo =
| Omit<ImageInfoFail, 'status' | 'stateName'>
| Omit<ImageInfoSuccess, 'status' | 'stateName'>
| Omit<ImageInfoError, 'status' | 'stateName'>;
| Omit<ImageInfoError, 'status' | 'stateName'>
| Omit<ImageInfoPageSuccess, 'status' | 'stateName'>;

export type AssertViewResult = AssertViewSuccess | ImageDiffError | NoRefImageError;

Expand Down
Loading

0 comments on commit 232ba8c

Please sign in to comment.