Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/approve button #1150

Merged
merged 6 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* scenario display filtering
* reference, test, visual diff inspector
* cool scrubber thingy
* approving a test

![BackstopJS browser report](http://garris.github.io/BackstopJS/assets/backstopjs_new_ui_.png)

Expand Down Expand Up @@ -863,7 +864,7 @@ _Of course you can alternatively change your default config to save these files


### Changing screenshot filename formats
One of the things Backstop does for you is manage all your screenshot files. Backstop uses a specific file-nameing scheme to make this work. Changing this scheme is of course NOT RECOMMENDED. That said -- if you have an overwhelming need, then you can modify this behavior using the `fileNameTemplate` property. The default pattern is shown below where the labels in braces are replaced with internal values during runtime.
One of the things Backstop does for you is manage all your screenshot files. Backstop uses a specific file-nameing scheme to make this work. Changing this scheme is of course NOT RECOMMENDED. That said -- if you have an overwhelming need, then you can modify this behavior using the `fileNameTemplate` property. The default pattern is shown below where the labels in braces are replaced with internal values during runtime.

```js
{
Expand Down Expand Up @@ -909,7 +910,12 @@ Here's some suggestions if you want to work on the HTML report locally...
- The HTML front end is a React app. It lives in `/compare/src/`

- The workflow is as follows from the backstopjs root
- Run a test with this...
- Start a remote server if you need to work with the approving tests flow

```
cd test/configs/ && node ../../cli/index.js remote
```
- Open another shell and run a test with this...

```
npm run sanity-test
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 7 additions & 7 deletions compare/output/index_bundle.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions compare/src/actions/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
export const approveTest = id => {
return {
type: 'APPROVE_TEST',
id
};
};

export const filterTests = status => {
return {
type: 'FILTER_TESTS',
Expand Down
116 changes: 116 additions & 0 deletions compare/src/components/molecules/ApproveButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { approveTest, filterTests } from '../../actions';
import { colors, fonts } from '../../styles';

const REMOTE_HOST = 'http://127.0.0.1';
const REMOTE_PORT = 3000;
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an open issue in ember-backstop to make backstop run on a port other than the default port 3000. Will having this as a constant make it harder to implement that ?

Copy link
Owner

@garris garris Feb 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great point. When Backstop opens a report (either internally or via user on cli) it runs openReport.js -- notice here is where the port is defined...

const remoteReportUrl = `http://127.0.0.1:3000/${config.compareReportURL}?remote`;

So, we should be using your port bar there.

And it is probably just a matter of swapping that hard coded port 3000 with a dynamic one from the passed in config object.

AN ADDITIONAL THING TO NOTE: This also happens to be where we explicitly tell the report UI that it is in REMOTE mode. Some features, like Approve will only work in remote mode (i.e. when backstopRemote is running). I don't remember if the UI has a global variable to track this -- but it needs one. And the Approve option should not be shown unless we are in remote mode.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1276stella ☝️

const APPROVE_STATUS_TO_LABEL_MAP = Object.freeze({
INITIAL: 'Approve',
PENDING: 'Pending...',
APPROVED: 'Approved',
FAILED: 'Approve'
});

const Button = styled.button`
font-size: 20px;
line-height: 40px;
font-family: ${fonts.latoRegular};
background-color: ${colors.green};
border: none;
border-radius: 3px;
color: ${colors.white};
padding: 0px 30px;
margin-bottom: 10px;

&:hover {
cursor: pointer;
}

&:disabled {
background-color: ${colors.bodyColor};
color: ${colors.secondaryText};
cursor: default;
}
`;

const ErrorMsg = styled.p`
word-wrap: break-word;
font-family: monospace;
background: rgb(251, 234, 234);
padding: 2ex;
color: brown;
`;

class ApproveButton extends React.Component {
constructor (props) {
super(props);
this.approve = this.approve.bind(this);
this.state = {
approveStatus: 'INITIAL',
errorMsg: null
};
}

async approve () {
const { fileName } = this.props;
const url = `${REMOTE_HOST}:${REMOTE_PORT}/approve?filter=${fileName}`;
this.setState({ approveStatus: 'PENDING' });

try {
const response = await fetch(url, {
method: 'POST'
});

if (response.ok) {
this.setState({ approveStatus: 'APPROVED' });
this.props.approveTest(this.props.currentId, this.props.filterStatus);
} else {
const body = await response.json();
this.setState({ approveStatus: 'FAILED', errorMsg: body.error });
}
} catch (err) {
this.setState({
approveStatus: 'FAILED',
errorMsg: `${err.message}. Please check your network.`
});
}
}

render () {
const { approveStatus, errorMsg } = this.state;

return (
<div>
<Button onClick={this.approve} disabled={approveStatus === 'APPROVED'}>
{APPROVE_STATUS_TO_LABEL_MAP[this.state.approveStatus]}
</Button>
{approveStatus === 'FAILED' && (
<ErrorMsg>BACKSTOP ERROR: {errorMsg}</ErrorMsg>
)}
</div>
);
}
}

const mapStateToProps = state => {
return {
filterStatus: state.tests.filterStatus
};
};

const mapDispatchToProps = dispatch => {
return {
approveTest: (id, filterStatus) => {
dispatch(approveTest(id));
dispatch(filterTests(filterStatus));
}
};
};

const ApproveButtonContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ApproveButton);
export default ApproveButtonContainer;
21 changes: 18 additions & 3 deletions compare/src/components/molecules/FiltersSwitch.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,23 @@ function ButtonsFilter (props) {
class FiltersSwitch extends React.Component {
render () {
let tests = this.props.tests;
let availableStatus = this.props.availableStatus;
let availableStatus = [
{
id: 'all',
label: 'all',
count: tests.all.length
},
{
id: 'pass',
label: 'passed',
count: tests.all.filter(e => e.status === 'pass').length
},
{
id: 'fail',
label: 'failed',
count: tests.all.filter(e => e.status === 'fail').length
}
];

return (
<ButtonsWrapper>
Expand All @@ -50,8 +66,7 @@ class FiltersSwitch extends React.Component {

const mapStateToProps = state => {
return {
tests: state.tests,
availableStatus: state.availableStatus
tests: state.tests
};
};

Expand Down
2 changes: 2 additions & 0 deletions compare/src/components/organisms/TestCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import NavButtons from '../atoms/NavButtons';

// molecules
import TestImages from '../molecules/TestImages';
import ApproveButton from '../molecules/ApproveButton';

const CardWrapper = styled.div`
position: relative;
Expand Down Expand Up @@ -42,6 +43,7 @@ export default class TestCard extends React.Component {

return (
<CardWrapper id={this.props.id} status={status}>
{status === 'fail' && <ApproveButton fileName={info.fileName} currentId={this.props.numId} />}
{!onlyText && (
<NavButtons currentId={this.props.numId} lastId={this.props.lastId} />
)}
Expand Down
8 changes: 0 additions & 8 deletions compare/src/reducers/availableStatus.js

This file was deleted.

2 changes: 0 additions & 2 deletions compare/src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ import { combineReducers } from 'redux';
import tests from './tests';
import suiteInfo from './suiteInfo';
import layoutSettings from './layoutSettings';
import availableStatus from './availableStatus';
import scrubber from './scrubber';

const rootReducer = combineReducers({
suiteInfo,
tests,
availableStatus,
scrubber,
layoutSettings
});
Expand Down
9 changes: 9 additions & 0 deletions compare/src/reducers/tests.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
const tests = (state = {}, action) => {
switch (action.type) {
case 'APPROVE_TEST':
return Object.assign({}, state, {
all: state.all.map((test, i) => {
if (i === action.id) {
return Object.assign({}, test, { status: 'pass' });
}
return test;
})
});
case 'FILTER_TESTS':
if (action.status !== 'all') {
return Object.assign({}, state, {
Expand Down
17 changes: 0 additions & 17 deletions compare/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,6 @@ const defaultState = {
filtered: window.tests.tests,
filterStatus: 'all'
},
availableStatus: [
{
id: 'all',
label: 'all',
count: window.tests.tests.length
},
{
id: 'pass',
label: 'passed',
count: window.tests.tests.filter(e => e.status === 'pass').length
},
{
id: 'fail',
label: 'failed',
count: window.tests.tests.filter(e => e.status === 'fail').length
}
],
scrubber: {
visible: false,
mode: 'scrub',
Expand Down
61 changes: 61 additions & 0 deletions core/util/remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const fs = require('./fs');

/**
* Extract jsonReport from the jsonpReport
*
* @param {String} jsonpReport - jsonpReport `report(${jsonReport});`
* @return {Object} an object of jsonReport
*/
function extractReport (jsonpReport) {
const jsonReport = jsonpReport.substring(7, jsonpReport.length - 2);
return JSON.parse(jsonReport);
}

/**
* Helper function to modify the test status of the JSONP report based on the approved file name.
*
* @param {String} originalJsonpReport - jsonpReport `report(${jsonReport});`
* @param {String} approvedFileName - the name of the screenshot that is approved
* @return {String} jsonpReport - modified jsonpReport
*/
function modifyJsonpReportHelper (originalJsonpReport, approvedFileName) {
const report = extractReport(originalJsonpReport);
report.tests.forEach(test => {
if (test.pair.fileName === approvedFileName) {
test.status = 'pass';
delete test.pair.diffImage;
1276stella marked this conversation as resolved.
Show resolved Hide resolved
}
return test;
});

const jsonReport = JSON.stringify(report, null, 2);
const jsonpReport = `report(${jsonReport});`;
return jsonpReport;
}

/**
* Modify the test status of the JSONP report based on the approved file name. JSONP is used
* to create the Backstop report in browser. This function ensures the UI consistency after
* a user apporves a test in browser and refreshes the report without running a test.
*
* @param {Object} params - the input params
* @param {String} params.reportConfigFilename - the path to the html report config file
* @param {String} params.approvedFileName - the name of the screenshot that is approved
* @return {Promise}
*/
async function modifyJsonpReport ({ reportConfigFilename, approvedFileName }) {
return fs
.readFile(reportConfigFilename, 'utf8')
.then(content => {
const jsonpReport = modifyJsonpReportHelper(content[0], approvedFileName);
return fs.writeFile(reportConfigFilename, jsonpReport);
})
.catch(err => {
throw new Error(`Failed to modify the report. ${err.message}.`);
});
}

module.exports = {
modifyJsonpReport,
modifyJsonpReportHelper
};
Loading