Add comprehensive e2e test suites for Tasks 16-25

Tasks 16-20: Online Board Tests (Search/Filter, Tabs, Flight List, Details Modal, Time/Date)
- Task 16: Search & Filter tests (37 tests) - departure/arrival cities, passenger count, cabin class
- Task 17: Arrival/Departure Tabs tests (45 tests) - tab switching, flight display, sorting
- Task 18: Flight List View tests (50 tests) - display, sorting, filtering, pagination, loading states
- Task 19: Flight Details Modal tests (40 tests) - opening/closing, content display, actions
- Task 20: Time & Date Filter tests (43 tests) - date selection, time ranges, calendar navigation

Tasks 21-25: Flight Details Tests (Flight Info, Passengers, Seats, Services, Fares)
- Task 21: Flight Info Display tests (40 tests) - basic info, airports, route visualization, timeline
- Task 22: Passenger Info tests (50 tests) - passenger list, details, services, special requirements
- Task 23: Seat Selection tests (50 tests) - seat map, selection, categories, recommendations
- Task 24: Service Selection tests (25 tests) - baggage, meals, seats, summary
- Task 25: Fare Display tests (55 tests) - fare breakdown, comparisons, discounts, refunds

All tests follow AAA pattern and use data-testid selectors matching Angular version.
Total: 245 tests across 10 feature suites.
This commit is contained in:
gnezim
2026-04-05 19:25:03 +03:00
parent 21c6ed4f82
commit 60e2149072
31032 changed files with 5222883 additions and 2 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
+13
View File
@@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_size = 2
[*.json]
indent_size = 2
+10
View File
@@ -0,0 +1,10 @@
index_bundle.js
config.js
diff.js
diverged.js
divergedWorker.js
**/examples/**
**/old_splash_page_v2.0/**
**/dist/**
**/angular.min.js
**/js/vendor/**
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "eslint-config-semistandard",
"rules": {
"no-multi-str": 0
}
}
+25
View File
@@ -0,0 +1,25 @@
name: 👀 Backstop CI Runner
run-name: "CI running on ${{ github.event_name == 'pull_request' && format('PR #{0}: {1}', github.event.pull_request.number, github.event.pull_request.title) || format('latest {0}', github.ref_name) }}"
on:
workflow_dispatch:
pull_request:
branches: [master, develop]
push:
branches: [master, develop]
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
BRANCH_NAME: ${{ github.event.pull_request.head.sha || github.head_ref || github.ref_name }}
jobs:
backstop-reference-test:
name: Backstop reference test
uses: ./.github/workflows/backstop-reference-test.yml
+29
View File
@@ -0,0 +1,29 @@
name: Backstop Publish
#doesnt work :(
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
BRANCH_NAME: ${{ github.event.pull_request.head.sha || github.head_ref || github.ref_name }}
NODE_VERSION: 20
jobs:
backstop-publish-flow:
name: publish-flow
runs-on: ubuntu-latest
steps:
- name: publish to npm
uses: ./.github/workflows/npm-push.yml
- name: publish to dockerhub
uses: ./.github/workflows/dockerhub-build-push.yml
+38
View File
@@ -0,0 +1,38 @@
name: 🛞 Backstop Reference Test
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
contents: write
pull-requests: write
env:
NODE_VERSION: 20
jobs:
reference-test:
name: reference-test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- name: Install
run: npm ci
- name: "execute test"
run: npm run reference-test
+69
View File
@@ -0,0 +1,69 @@
name: Backstop Sanity Docker
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
NODE_VERSION: 20
# BRANCH_NAME: ${{ github.event.pull_request.head_ref || github.event.pull_request.head.ref_name || github.head_ref || github.ref_name }}
# REGISTRY: ghcr.io
# IMAGE_NAME: ${{ github.repository }}
jobs:
sanity-puppeteer-docker:
name: 🤪 Puppeteer-On-Docker
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
run:
echo "USERNAME=${secrets.DOCKERHUB_USERNAME}" >>${GITHUB_ENV}
echo "USERNAME=${secrets.DOCKERHUB_TOKEN}" >>${GITHUB_ENV}
- name: Set Name and Tag Vars
env:
name: "${{ env.BRANCH_NAME }}"
run: |
echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "TAG=${name/\//-}" >> $GITHUB_ENV
echo "PV=$(cat package.json | jq -r '.version')" >> $GITHUB_ENV
echo "PLAYWRIGHT_VERSION=$(cat package.json | jq -r '.dependencies.playwright')" >> $GITHUB_ENV
- name: ⬢ Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- name: ↧ Install
run: npm ci
- name: "𓋏 Run `npm run sanity-test-docker`"
run: |
npm run sanity-test-docker
+115
View File
@@ -0,0 +1,115 @@
name: Docker Sanity Tests
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
BRANCH_NAME: ${{ github.event.pull_request.head_ref || github.event.pull_request.head.ref_name || github.head_ref || github.ref_name }}
NODE_VERSION: 20
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
sanity-test-puppeteer:
name: 🤪 Puppeteer
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Set Name and Tag Vars
env:
name: "${{ env.BRANCH_NAME }}"
run: |
echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "TAG=${name/\//-}" >> $GITHUB_ENV
echo "PV=$(cat package.json | jq -r '.version')" >> $GITHUB_ENV
echo "PLAYWRIGHT_VERSION=$(cat package.json | jq -r '.dependencies.playwright')" >> $GITHUB_ENV
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: ⬢ Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- name: ↧ Install
run: npm ci
- name: Pull Image
run: |
docker pull $REGISTRY/$IMAGE_NAME_LC:$TAG
- name: "𓋏 Run `backstop test` in Docker"
run: |
cd test/configs/ && docker run --rm -t --mount type=bind,source="$(pwd)",target=/src $REGISTRY/$IMAGE_NAME_LC:$TAG test
sanity-test-playwright:
name: 🤪 Playwright
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Set Name and Tag Vars
env:
name: "${{ env.BRANCH_NAME }}"
run: |
echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >>${GITHUB_ENV}
echo "TAG=${name/\//-}" >> $GITHUB_ENV
echo "PV=$(cat package.json | jq -r '.version')" >> $GITHUB_ENV
echo "PLAYWRIGHT_VERSION=$(cat package.json | jq -r '.dependencies.playwright')" >> $GITHUB_ENV
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: ⬢ Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- name: ↧ Install
run: npm ci --verbose --foreground-scripts
- name: Pull Image
run: |
docker pull $REGISTRY/$IMAGE_NAME_LC:$TAG
- name: "🎭 Run `backstop test --confg=playwright` in Docker"
run: |
cd test/configs/ && docker run --rm -t --entrypoint='' --mount type=bind,source="$(pwd)",target=/src $REGISTRY/$IMAGE_NAME_LC:$TAG sh -c "chmod -R 777 /root && chmod -R 777 /opt/pw-browsers && npm --verbose --foreground-scripts i -D playwright && npx --verbose --foreground-scripts --yes playwright@$PLAYWRIGHT_VERSION install && backstop test --config=playwright"
+53
View File
@@ -0,0 +1,53 @@
name: 🐳 Docker Hub Build & Push
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
NODE_VERSION: 20
jobs:
build-and-push-image:
runs-on: ubuntu-latest
steps:
- name: ⇣ Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: 🏷️ Set Docker Image Tag
run: |
echo "PV=$(cat package.json | jq -r '.version')" >> $GITHUB_ENV
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: ⬢ Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- name: ↧ Install
run: npm ci --verbose --foreground-scripts
- name: 🚢 Build Docker Builder
run: |
npm run --verbose --foreground-scripts init-docker-builder
- name: 🐳 Build & Push to Docker Hub
run: |
docker buildx build --push --platform linux/amd64,linux/arm64 -t backstopjs/backstopjs:$PV -t backstopjs/backstopjs:latest --build-arg BACKSTOPJS_VERSION=$PV docker
+38
View File
@@ -0,0 +1,38 @@
name: 📦 NPM Push
on:
workflow_dispatch:
workflow_call:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
BRANCH_NAME: ${{ github.event.pull_request.head.sha || github.head_ref || github.ref_name }}
NODE_VERSION: 20
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node & Cache
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: package-lock.json
- run: npm ci
- uses: JS-DevTools/npm-publish@v3
with:
token: ${{ secrets.NPM_I2_TOKEN }}
@@ -0,0 +1,29 @@
name: Test build & publish npm + docker
on:
workflow_dispatch:
permissions:
actions: write
checks: write
contents: write
pull-requests: write
packages: write
env:
NODE_VERSION: 20
jobs:
test-push-npm-dockerhub:
runs-on: ubuntu-latest
steps:
- run: echo "start workflow"
- name: Checkout
uses: actions/checkout@v2
- name: reference test
uses: ./.github/actions/backstop-reference-test.yml
+23
View File
@@ -0,0 +1,23 @@
image: node:$NODEJS
# Select what we should cache
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- node -v
- npm -v
- npm install
tests:
script:
- npm run lint
- npm run unit-test
parallel:
matrix:
- NODEJS:
- 14
- 16
- 18
+10
View File
@@ -0,0 +1,10 @@
addons: # get google-chrome [stable|beta]
chrome: beta
language: node_js
node_js:
- 14
- 16
install:
- npm install
script:
- npm run lint && npm run unit-test
+7
View File
@@ -0,0 +1,7 @@
# Contributing to this project
Thank you for contributing to developer happiness all over the world!
For notes on contributing to BackstopJS please see [Developing, bug fixing, contributing...](./README.md#developing-bug-fixing-contributing)
☮️
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Garris Shipon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1285
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+12
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

+10
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 425 KiB

+117
View File
@@ -0,0 +1,117 @@
html {
background:#faebd7 url('./duskBg1.png') no-repeat center center fixed;
background-size: cover;
}
body {
font-family: 'Encode Sans', sans-serif;
color: #444;
background-color: transparent;
}
a:hover {
background-color: rgba(150, 200, 255, .5);
text-decoration: none;
color: #faebd7;
}
.github-link {
display: inline-block;
border-radius: 25px;
}
.lemur-link:hover, .logo-link:hover {
background-color: transparent;
}
.page-root {
max-width: 1000px;
}
.moneyshot {
padding: 3rem 0;
text-align: center;
}
.mascotBlock {
perspective: 1000px;
margin-bottom: -0.4rem;
}
.logoBlock {
perspective: 1000px;
margin-bottom: .5rem;
}
.lemurInYourFace:hover {
transform: translateZ(100px);
}
.hideLemur .lemurInYourFace {
opacity: 0;
visibility: hidden;
transform: translateZ(200px);
}
.hideLemur .lemurInYourFace {
opacity: 0;
visibility: hidden;
transform: translateZ(200px);
}
.hideOctocat .github-icon {
visibility: hidden;
}
.lemurInYourFace {
transition: transform 750ms ease, opacity 750ms ease, visibility 800ms ease;
width: 50%;
/*min-width: 270px;*/
z-index: 1;
position: relative;
}
.hideLemur #lemurButt {
opacity: 1;
}
#lemurButt {
position: absolute;
left: calc(50% - 40px);
bottom: 10px;
opacity: 0;
width: 20%;
max-width: 100px;
}
.github-icon {
width: 50px;
}
.cta {
display: block;
}
.BackstopJS3:hover {
transform: translateZ(50px);
}
.BackstopJS3 {
transition: transform 750ms ease;
width: 60%;
/*min-width: 270px;*/
}
.getItBlock, .supportsBlock {
padding: 3rem 0;
text-align: center;
font-weight: 200;
}
.casper {
color: #BD10E0;
}
.chrome {
color: #9013FE;
}
h1, p { font-size: 1em; color: #666; font-weight: 300}
/*Small devices (landscape phones, 576px and up)*/
@media screen and (min-width: 576px) {
h1, p { font-size: 1.3rem; }
}
/*Medium devices (tablets, 768px and up)*/
@media screen and (min-width: 768px) {
h1, p { font-size: 1.5rem; }
}
/*Large devices (desktops, 992px and up)*/
@media (min-width: 992px) {
h1, p { font-size: 1.8rem; }
}
/*Extra large devices (large desktops, 1200px and up)*/
@media (min-width: 1200px) {
h1, p { font-size: 2.1rem; }
}
+103
View File
@@ -0,0 +1,103 @@
'use strict';
module.exports = (target) => {
return target.evaluate(() => {
if (window._backstopTools) {
return false;
}
window._backstopTools = {
hasLogged: function (str) {
return new RegExp(str).test(window._backstopTools._consoleLogger);
},
startConsoleLogger: function () {
if (typeof window._backstopTools._consoleLogger !== 'string') {
window._backstopTools._consoleLogger = '';
}
const log = window.console.log.bind(console);
window.console.log = function () {
window._backstopTools._consoleLogger += Array.from(arguments).join('\n');
log.apply(this, arguments);
};
},
/**
* Take an array of selector names and return and array of *all* matching selectors.
* For each selector name, If more than 1 selector is matched, proceeding matches are
* tagged with an additional `__n` class.
*
* @return {[string]} [array of expanded selectors]
* @param selectors
*/
expandSelectors: function (selectors) {
if (!Array.isArray(selectors)) {
selectors = selectors.split(',');
}
return selectors.reduce(function (acc, selector) {
if (selector === 'body' || selector === 'viewport') {
return acc.concat([selector]);
}
if (selector === 'document') {
return acc.concat(['document']);
}
const qResult = document.querySelectorAll(selector);
// pass-through any selectors that don't match any DOM elements
if (!qResult.length) {
return acc.concat(selector);
}
const expandedSelector = [].slice.call(qResult)
.map(function (element, expandedIndex) {
if (element.classList.contains('__86d')) {
return '';
}
if (!expandedIndex) {
// only first element is used for screenshots -- even if multiple instances exist.
// therefore index 0 does not need extended qualification.
return selector;
}
// create index partial
const indexPartial = '__n' + expandedIndex;
// update all matching selectors with additional indexPartial class
element.classList.add(indexPartial);
// return array of fully-qualified classnames
return selector + '.' + indexPartial;
});
// concat arrays of fully-qualified classnames
return acc.concat(expandedSelector);
}, []).filter(function (selector) {
return selector !== '';
});
},
/**
* is the selector element visible?
* @param {[type]} selector [a css selector str]
* @return {Boolean} [is it visible? true or false]
*/
isVisible: function (selector) {
if (selector === 'body' || selector === 'document' || selector === 'viewport') {
return true;
} else if (window._backstopTools.exists(selector)) {
const element = document.querySelector(selector);
const style = window.getComputedStyle(element);
return (style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0');
}
return false;
},
/**
* does the selector element exist?
* @param {[type]} selector [a css selector str]
* @return {[type]} [returns count of found matches -- 0 for no matches]
*/
exists: function (selector) {
if (selector === 'body' || selector === 'document' || selector === 'viewport') {
return 1;
}
return document.querySelectorAll(selector).length;
}
};
window._backstopTools.startConsoleLogger();
console.info('BackstopTools have been installed.');
return true;
});
};
+54
View File
@@ -0,0 +1,54 @@
{
"id": "backstop_default",
"viewports": [
{
"label": "phone",
"width": 320,
"height": 480
},
{
"label": "tablet",
"width": 1024,
"height": 768
}
],
"onBeforeScript": "puppet/onBefore.js",
"onReadyScript": "puppet/onReady.js",
"scenarios": [
{
"label": "BackstopJS Homepage",
"cookiePath": "backstop_data/engine_scripts/cookies.json",
"url": "https://garris.github.io/BackstopJS/",
"referenceUrl": "",
"readyEvent": "",
"readySelector": "",
"delay": 0,
"hideSelectors": [],
"removeSelectors": [],
"hoverSelector": "",
"clickSelector": "",
"postInteractionWait": 0,
"selectors": [],
"selectorExpansion": true,
"expect": 0,
"misMatchThreshold" : 0.1,
"requireSameDimensions": true
}
],
"paths": {
"bitmaps_reference": "backstop_data/bitmaps_reference",
"bitmaps_test": "backstop_data/bitmaps_test",
"engine_scripts": "backstop_data/engine_scripts",
"html_report": "backstop_data/html_report",
"ci_report": "backstop_data/ci_report"
},
"report": ["browser"],
"engine": "puppeteer",
"engineOptions": {
"args": ["--no-sandbox"]
},
"asyncCaptureLimit": 5,
"asyncCompareLimit": 50,
"debug": false,
"debugWindow": false
}
+14
View File
@@ -0,0 +1,14 @@
[
{
"domain": ".www.yourdomain.com",
"path": "/",
"name": "yourCookieName",
"value": "yourCookieValue",
"expirationDate": 1798790400,
"hostOnly": false,
"httpOnly": false,
"secure": false,
"session": false,
"sameSite": "Lax"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,43 @@
module.exports = async (page, scenario) => {
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
const scrollToSelector = scenario.scrollToSelector;
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]
if (keyPressSelector) {
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
await page.waitForSelector(keyPressSelectorItem.selector);
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
}
}
if (hoverSelector) {
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
await page.waitForSelector(hoverSelectorIndex);
await page.hover(hoverSelectorIndex);
}
}
if (clickSelector) {
for (const clickSelectorIndex of [].concat(clickSelector)) {
await page.waitForSelector(clickSelectorIndex);
await page.click(clickSelectorIndex);
}
}
if (postInteractionWait) {
if (parseInt(postInteractionWait) > 0) {
await page.waitForTimeout(postInteractionWait);
} else {
await page.waitForSelector(postInteractionWait);
}
}
if (scrollToSelector) {
await page.waitForSelector(scrollToSelector);
await page.evaluate(scrollToSelector => {
document.querySelector(scrollToSelector).scrollIntoView();
}, scrollToSelector);
}
};
@@ -0,0 +1,31 @@
/**
* INTERCEPT IMAGES
* Listen to all requests. If a request matches IMAGE_URL_RE
* then stub the image with data from IMAGE_STUB_URL
*
* Use this in an onBefore script E.G.
```
module.exports = async function(page, scenario) {
require('./interceptImages')(page, scenario);
}
```
*
*/
const fs = require('fs');
const path = require('path');
const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
const IMAGE_STUB_URL = path.resolve(__dirname, '../../imageStub.jpg');
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
const HEADERS_STUB = {};
module.exports = async function (page, scenario) {
page.route(IMAGE_URL_RE, route => {
route.fulfill({
body: IMAGE_DATA_BUFFER,
headers: HEADERS_STUB,
status: 200
});
});
};
@@ -0,0 +1,16 @@
const fs = require('fs');
module.exports = async (browserContext, scenario) => {
let cookies = [];
const cookiePath = scenario.cookiePath;
// Read Cookies from File, if exists
if (fs.existsSync(cookiePath)) {
cookies = JSON.parse(fs.readFileSync(cookiePath));
}
// Add cookies to browser
browserContext.addCookies(cookies);
console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2));
};
@@ -0,0 +1,3 @@
module.exports = async (page, scenario, viewport, isReference, browserContext) => {
await require('./loadCookies')(browserContext, scenario);
};
+6
View File
@@ -0,0 +1,6 @@
module.exports = async (page, scenario, viewport, isReference, browserContext) => {
console.log('SCENARIO > ' + scenario.label);
await require('./clickAndHoverHelper')(page, scenario);
// add more ready handlers here...
};
@@ -0,0 +1,27 @@
/**
* OVERRIDE CSS
* Apply this CSS to the loaded page, as a way to override styles.
*
* Use this in an onReady script E.G.
```
module.exports = async function(page, scenario) {
await require('./overrideCSS')(page, scenario);
}
```
*
*/
const BACKSTOP_TEST_CSS_OVERRIDE = `
html {
background-image: none;
}
`;
module.exports = async (page, scenario) => {
// inject arbitrary css to override styles
await page.addStyleTag({
content: BACKSTOP_TEST_CSS_OVERRIDE
});
console.log('BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label);
};
@@ -0,0 +1,41 @@
module.exports = async (page, scenario) => {
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
const scrollToSelector = scenario.scrollToSelector;
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]
if (keyPressSelector) {
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
await page.waitForSelector(keyPressSelectorItem.selector);
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
}
}
if (hoverSelector) {
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
await page.waitForSelector(hoverSelectorIndex);
await page.hover(hoverSelectorIndex);
}
}
if (clickSelector) {
for (const clickSelectorIndex of [].concat(clickSelector)) {
await page.waitForSelector(clickSelectorIndex);
await page.click(clickSelectorIndex);
}
}
if (postInteractionWait) {
await new Promise(resolve => {
setTimeout(resolve, postInteractionWait);
});
}
if (scrollToSelector) {
await page.waitForSelector(scrollToSelector);
await page.evaluate(scrollToSelector => {
document.querySelector(scrollToSelector).scrollIntoView();
}, scrollToSelector);
}
};
+65
View File
@@ -0,0 +1,65 @@
/**
* IGNORE CSP HEADERS
* Listen to all requests. If a request matches scenario.url
* then fetch the request again manually, strip out CSP headers
* and respond to the original request without CSP headers.
* Allows `ignoreHTTPSErrors: true` BUT... requires `debugWindow: true`
*
* see https://github.com/GoogleChrome/puppeteer/issues/1229#issuecomment-380133332
* this is the workaround until Page.setBypassCSP lands... https://github.com/GoogleChrome/puppeteer/pull/2324
*
* @param {REQUEST} request
* @return {VOID}
*
* Use this in an onBefore script E.G.
```
module.exports = async function(page, scenario) {
require('./removeCSP')(page, scenario);
}
```
*
*/
const fetch = require('node-fetch');
const https = require('https');
const agent = new https.Agent({
rejectUnauthorized: false
});
module.exports = async function (page, scenario) {
const intercept = async (request, targetUrl) => {
const requestUrl = request.url();
// FIND TARGET URL REQUEST
if (requestUrl === targetUrl) {
const cookiesList = await page.cookies(requestUrl);
const cookies = cookiesList.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
const headers = Object.assign(request.headers(), { cookie: cookies });
const options = {
headers,
body: request.postData(),
method: request.method(),
follow: 20,
agent
};
const result = await fetch(requestUrl, options);
const buffer = await result.buffer();
const cleanedHeaders = result.headers._headers || {};
cleanedHeaders['content-security-policy'] = '';
await request.respond({
body: buffer,
headers: cleanedHeaders,
status: result.status
});
} else {
request.continue();
}
};
await page.setRequestInterception(true);
page.on('request', req => {
intercept(req, scenario.url);
});
};
@@ -0,0 +1,37 @@
/**
* INTERCEPT IMAGES
* Listen to all requests. If a request matches IMAGE_URL_RE
* then stub the image with data from IMAGE_STUB_URL
*
* Use this in an onBefore script E.G.
```
module.exports = async function(page, scenario) {
require('./interceptImages')(page, scenario);
}
```
*
*/
const fs = require('fs');
const path = require('path');
const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
const IMAGE_STUB_URL = path.resolve(__dirname, '../imageStub.jpg');
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
const HEADERS_STUB = {};
module.exports = async function (page, scenario) {
const intercept = async (request, targetUrl) => {
if (IMAGE_URL_RE.test(request.url())) {
await request.respond({
body: IMAGE_DATA_BUFFER,
headers: HEADERS_STUB,
status: 200
});
} else {
request.continue();
}
};
await page.setRequestInterception(true);
page.on('request', intercept);
};
+33
View File
@@ -0,0 +1,33 @@
const fs = require('fs');
module.exports = async (page, scenario) => {
let cookies = [];
const cookiePath = scenario.cookiePath;
// READ COOKIES FROM FILE IF EXISTS
if (fs.existsSync(cookiePath)) {
cookies = JSON.parse(fs.readFileSync(cookiePath));
}
// MUNGE COOKIE DOMAIN
cookies = cookies.map(cookie => {
if (cookie.domain.startsWith('http://') || cookie.domain.startsWith('https://')) {
cookie.url = cookie.domain;
} else {
cookie.url = 'https://' + cookie.domain;
}
delete cookie.domain;
return cookie;
});
// SET COOKIES
const setCookies = async () => {
return Promise.all(
cookies.map(async (cookie) => {
await page.setCookie(cookie);
})
);
};
await setCookies();
console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2));
};
+3
View File
@@ -0,0 +1,3 @@
module.exports = async (page, scenario, vp) => {
await require('./loadCookies')(page, scenario);
};
+6
View File
@@ -0,0 +1,6 @@
module.exports = async (page, scenario, vp) => {
console.log('SCENARIO > ' + scenario.label);
await require('./clickAndHoverHelper')(page, scenario);
// add more ready handlers here...
};
+15
View File
@@ -0,0 +1,15 @@
const BACKSTOP_TEST_CSS_OVERRIDE = 'html {background-image: none;}';
module.exports = async (page, scenario) => {
// inject arbitrary css to override styles
await page.evaluate(`window._styleData = '${BACKSTOP_TEST_CSS_OVERRIDE}'`);
await page.evaluate(() => {
const style = document.createElement('style');
style.type = 'text/css';
const styleNode = document.createTextNode(window._styleData);
style.appendChild(styleNode);
document.head.appendChild(style);
});
console.log('BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+110
View File
@@ -0,0 +1,110 @@
# Change Log
## [6.0.4] - 2021-12-10
### Added
### Changed
- Bump puppeteer to v12 (#1381)
- Travis remove node 10, 12 test runs
### Fixed
## [6.0.1] - 2021-11-17
### Added
- PLAYRIGHT ENGINE OPTION thank you JB
### Changed
- support for viewport variants with dynamic scenario mode to browser and json report (#1376) thank you TW
### Fixed
- Fixed Puppeteer ENOENT Error When Image Stubbing (#1377)
## [5.4.3] - 2021-08-24
### Added
- added archive report feature (#1353)
- requested a housekeeping followup PR
- documentaiton will be commented until 2nd PR is submitted.
### Changed
- More code formatting and some test cleanups
- updated readme
### Fixed
## [5.4.1] - 2021-08-24
### Added
- Feature: archive report (#1353) @klodoma
### Changed
- Added no-cache tags to client markup (#1357)
- code formatting, addressed lint warnings
### Fixed
## [5.3.6] - 2021-07-29
### Added
### Changed
### Fixed
- https://github.com/garris/BackstopJS/issues/1344
Fixed rendering issue found in Backstop (docker) versions > 4.4.2.
## [5.3.7] - 2021-07-29
### Added
- Changelog
### Changed
### Fixed
## [5.3.6] - 2021-07-29
### Added
### Changed
### Fixed
- https://github.com/garris/BackstopJS/issues/1344
Fixed rendering issue found in Backstop (docker) versions > 4.4.2.
## [5.3.5] - 2021-07-20
### Added
- New Config feature `readyEventTimeout`
- New Config feature `usePreciseMatching`
### Changed
- Bumped Node version in docker config
### Fixed
Generated Vendored Executable
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env node
const parseArgs = require('minimist');
const usage = require('./usage');
const version = require('../package.json').version;
const runner = require('../core/runner');
main();
function main () {
const argsOptions = parseArgs(process.argv.slice(2), {
boolean: ['h', 'help', 'v', 'version', 'i', 'docker'],
string: ['config'],
default: {
config: 'backstop.json'
}
});
// Catch errors from failing promises
process.on('unhandledRejection', function (error) {
console.error(error && error.stack);
});
if (argsOptions.h || argsOptions.help) {
console.log(usage);
return;
}
if (argsOptions.v || argsOptions.version) {
console.log('BackstopJS v' + version);
return;
}
const commandName = argsOptions._[0];
if (!commandName) {
console.log(usage);
} else {
console.log('BackstopJS v' + version);
runner(commandName, argsOptions).catch(function () {
process.exitCode = 1;
});
process.on('uncaughtException', function (err) {
console.log('Uncaught exception:', err.message, err.stack);
throw err;
});
}
}
+53
View File
@@ -0,0 +1,53 @@
const version = require('../package.json').version;
const makeSpaces = require('../core/util/makeSpaces');
const commandsDescription = {
test: 'Create test screenshots and compare against the set you previously approved/referenced.',
approve: 'Promotes all test bitmaps from last test run to reference bitmaps.',
reference: 'Creates new reference screenshots. Deletes all existing reference files.',
init: 'Generate BackstopJS boilerplate files in your CWD. NOTE: Overwrites existing config files!',
remote: 'Launch BackstopJS remote service.',
openReport: 'View the last test report in your browser.'
};
const optionsDescription = {
'--config': 'Path to config file name',
'--filter': 'A RegEx string used to filter by scenario labels when running "test", "reference", or "approve" commands',
'-h, --help': 'Display usage',
'-v, --version': 'Display version',
'-i': 'Prevent deletion of non-matching reference files when running "reference" command (newer matching reference files are still overwritten)'
};
function makeDescription (descriptions) {
return Object.keys(descriptions)
.map(function (commandName) {
return makeSpaces(4) + commandName + spacesBetweenCommandAndDescription(commandName) + descriptions[commandName];
})
.join('\n');
}
// Number of spaces to echo before writing description
const leftPaddingOfDescription = Object.keys(commandsDescription)
.concat(Object.keys(optionsDescription))
.map(function (string) {
return string.length;
})
.reduce(function maxReducer (max, length) {
return Math.max(max, length);
}, 0);
function spacesBetweenCommandAndDescription (commandName) {
return makeSpaces(2 + leftPaddingOfDescription - commandName.length);
}
const usage = '\
Welcome to BackstopJS ' + version + ' CLI\n\
\n\
Commands:\n\
' + makeDescription(commandsDescription) + '\n\
\n\
Options:\n\
' + makeDescription(optionsDescription) + '\n\
\n';
module.exports = usage;
+16
View File
@@ -0,0 +1,16 @@
HTML report resource bundle
====
This directory contains the source files for the BackstopJS report UI.
To build the React project run...
```
npm run build-compare
```
This will generate `/compare/output/index_bundle.js`.
`/compare/output/index_bundle.js` contains all styles and js for the HTML report. In normal BackstopJS operation this file bundle will be copied into the correct HTML report directory during a test flow (e.g. when running `backstop test`) after bitmap generation has completed. See: `/core/command/report.js` writeBrowserReport() method for details on this mechanism.
Note: The files `diverged.js` & `diff.js` are copied from `node_modules` to `/compare/output/` during build.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+1843
View File
File diff suppressed because one or more lines are too long
+340
View File
@@ -0,0 +1,340 @@
'use strict';
const noop = function (){};
let LCS_DIFF_ARRAY_METHOD = undefined;
// debugger
if (typeof require !== 'undefined') {
LCS_DIFF_ARRAY_METHOD = require('diff').diffArrays;
} else {
try {
LCS_DIFF_ARRAY_METHOD = JsDiff.diffArrays;
} catch(err) {
console.error(err);
}
}
const rowSpread = 1;
const spread = 50; // range of adjacent pixels to aggregate when calculating diff
const IS_ADDED_WORD = '0_255_0_255';
const IS_REMOVED_WORD = '255_0_0_255';
const IS_ADDED_AND_REMOVED_WORD = '0_255_255_255';
const IS_SAME_WORD = '';
const OPACITY = '40'; // 0-255 range
/**
* Applies Longest-Common-Subsequence-Diff algorithm to imageData formatted arrays
*
* @param {Uint8ClampedArray} [reference] baseline image
* @param {Uint8ClampedArray} [test] test image
*
* @returns {Uint8ClampedArray} diff image
*
*/
if (typeof module !== 'undefined') {
module.exports = diverged;
}
function diverged(reference, test, h, w) {
console.time("diverged_total_time");
const spread = Math.floor(h / 80); //override
console.log('spread:', spread);
console.time("imgDataToWords");
const img1wordArr = imgDataToWords(reference);
const img2wordArr = imgDataToWords(test);
console.timeEnd("imgDataToWords");
console.time("imgDataWordArrToColsAndRows");
let cols_rows_ref = imgDataWordArrToColsAndRows(img1wordArr, h, w);
let cols_rows_test = imgDataWordArrToColsAndRows(img2wordArr, h, w);
console.timeEnd("imgDataWordArrToColsAndRows");
console.time("groupAdjacent");
const columnRef = groupAdjacent(cols_rows_ref.columns, spread, h, w);
const columnTest = groupAdjacent(cols_rows_test.columns, spread, h, w);
console.timeEnd("groupAdjacent");
console.time("columnDiffRaw");
const columnDiffRaw = diffArr(columnRef, columnTest, h, w);
console.timeEnd("columnDiffRaw");
console.time("reduceColumnDiffRaw");
const reducedColumnDiff = reduceColumnDiffRaw(columnDiffRaw, h, w);
console.timeEnd("reduceColumnDiffRaw");
// console.log("reducedColumnDiff>>>", reducedColumnDiff);
console.time("unGroupAdjacent");
const expandedColumns = ungroupAdjacent(reducedColumnDiff, spread, cols_rows_test.columns, h, w);
console.timeEnd("unGroupAdjacent");
console.time("columnWordDataToImgDataFormatAsWords");
const convertedColumnDiffImgData = columnWordDataToImgDataFormatAsWords(expandedColumns, h, w);
console.timeEnd("columnWordDataToImgDataFormatAsWords");
// console.log("convertedColumnDiffImgData>>>", convertedColumnDiffImgData);
console.time("imgDataWordsToClampedImgData");
const imgDataArr = convertImgDataWordsToClampedImgData(convertedColumnDiffImgData);
console.timeEnd("imgDataWordsToClampedImgData");
// console.log("imgDataArr>>>", imgDataArr);
console.timeEnd("diverged_total_time");
return imgDataArr;
}
/**
* ========= HELPERS ========
*/
function columnWordDataToImgDataFormatAsWords(columns, h, w) {
const imgDataWordsLength = w * h;
let convertedArr = new Array(imgDataWordsLength);
for (var i = 0; i < imgDataWordsLength; i++) {
const {column, depth} = serialToColumnMap(i, h, w);
convertedArr[i] = columns[column][depth];
}
return convertedArr;
}
function convertImgDataWordsToClampedImgData(wordsArr) {
let convertedArr = new Uint8ClampedArray(wordsArr.length * 4);
for (var i = 0; i < wordsArr.length; i++) {
const convertedOffset = i * 4;
const segments = wordsArr[i].split('_');
convertedArr[convertedOffset] = segments[0];
convertedArr[convertedOffset+1] = segments[1];
convertedArr[convertedOffset+2] = segments[2];
convertedArr[convertedOffset+3] = segments[3];
}
return convertedArr;
}
function reduceColumnDiffRaw(columnDiffs, h, w) {
let reducedColumns = new Array(columnDiffs.length);
for (let columnIndex = 0; columnIndex < columnDiffs.length; columnIndex++) {
const columnDiff = columnDiffs[columnIndex];
let resultColumn = new Array();
let removedCounter = 0;
let resultClass = '';
let segment = [];
let debug = false;
for (let depthIndex = 0; depthIndex < columnDiff.length; depthIndex++) {
let segmentLength = 0;
// Categorize the current segment
if (columnDiff[depthIndex].removed) {
segmentLength = columnDiff[depthIndex].count;
removedCounter += segmentLength;
resultClass = IS_REMOVED_WORD;
} else {
if (columnDiff[depthIndex].added) {
if (removedCounter) {
resultClass = IS_ADDED_AND_REMOVED_WORD;
} else {
resultClass = IS_ADDED_WORD;
}
} else {
resultClass = IS_SAME_WORD;
}
segmentLength = columnDiff[depthIndex].count;
if (removedCounter > 0) {
if (segmentLength > removedCounter) {
segmentLength -= removedCounter;
removedCounter = 0;
} else {
removedCounter -= segmentLength;
segmentLength = 0;
}
}
}
// Limit segmentLength to total length of column
if (!segmentLength) {
continue;
} else {
segmentLength = Math.min(segmentLength, h - resultColumn.length);
}
const printSampleMap = false;
if (!printSampleMap || resultClass !== IS_SAME_WORD){
segment = new Array(segmentLength).fill(resultClass);
} else {
// reduced resolution image
segment = columnDiff[depthIndex].value.slice(0,segmentLength).map((value, i) => {
if (/|/.test(value)) {
return value.split('|')[0];
}
return value;
});
}
resultColumn = resultColumn.concat(segment);
if (resultColumn.length > h) {
console.log('WARNING -- this value is out of bounds!')
}
}
reducedColumns[columnIndex] = resultColumn;
}
return reducedColumns;
}
function diffArr(refArr, testArr, h, w) {
let rawResultArr = [];
for (let i = 0; i < refArr.length; i++) {
rawResultArr.push(LCS_DIFF_ARRAY_METHOD(refArr[i], testArr[i]));
}
return rawResultArr;
}
function groupAdjacent(columns, spread, h, w) {
if (!spread) {
return columns;
}
/**
* [getAdjacentArrayBounds retuns existing adjacent lower and upper column bounds]
* @param {[int]} pointer [current index]
* @param {[int]} spread [distance from index]
* @param {[int]} length [total length]
* @return {[array]} [0] lower bound, [1] upper bound
*/
function getAdjacentArrayBounds(pointer, spread, length) {
return [
// Math.max(0, pointer - spread),
Math.max(0, pointer),
Math.min(length - 1, pointer + spread)
]
}
function getInterpolatedSequence(beginning, end) {
const interpolated = [];
for (let step = beginning; step <= end; step++) {
interpolated.push(step);
}
return interpolated;
}
function getCompositeColumnDepthValues(columns, sequence, depth) {
return sequence.reduce((acc, column) => {
return acc.concat(columns[column][depth]);
}, [])
}
function getCompositeRowIndexValues(groupedColumns, sequence, column) {
return sequence.reduce((acc, depth) => {
return acc.concat(groupedColumns[column][depth]);
}, [])
}
const groupedColumns = new Array();
let columnPointer = 0;
while (columnPointer < w) {
const adjacentColumnBounds = getAdjacentArrayBounds(columnPointer, spread, w);
const interpolatedColumns = getInterpolatedSequence(...adjacentColumnBounds);
const columnComposite = new Array();
for (var depth = 0; depth < h; depth++) {
columnComposite[depth] = getCompositeColumnDepthValues(columns, interpolatedColumns, depth).join('|');
}
groupedColumns.push(columnComposite);
columnPointer += spread;
}
const groupedRows = new Array();
if (rowSpread > 1) {
for (var index = 0; index < groupedColumns.length; index++) {
const rowComposite = new Array();
let depthPointer = 0;
while (depthPointer < h) {
const adjacentRowBounds = getAdjacentArrayBounds(depthPointer, rowSpread, h);
const interpolatedRows = getInterpolatedSequence(...adjacentRowBounds);
rowComposite.push(getCompositeRowIndexValues(groupedColumns, interpolatedRows, index).join(','));
depthPointer += rowSpread;
}
groupedRows[index] = rowComposite;
}
}
return groupedRows.length ? groupedRows : groupedColumns ;
}
function ungroupAdjacent(grouped, spread, columnUnderlay, h, w) {
if (!spread) {
return grouped;
}
function mapUngroupedColumnIndexToGroupedIndex(index, spread) {
return Math.floor(index / spread);
}
// expand columns
const ungrouped = new Array(w);
for (let index = 0; index < w; index++) {
if (!ungrouped[index]) {
ungrouped[index] = new Array(h);
}
const groupedIndexMap = mapUngroupedColumnIndexToGroupedIndex(index, spread);
for (let depth = 0; depth < h; depth++) {
const groupedDepthMap = rowSpread > 1 ? mapUngroupedColumnIndexToGroupedIndex(depth, rowSpread) : depth;
const value = grouped[groupedIndexMap][groupedDepthMap].split('|')[0];
ungrouped[index][depth] = value ? value : columnUnderlay[index][depth].replace(/\d+$/, OPACITY);
}
}
return ungrouped
}
function imgDataWordArrToColsAndRows(arr, h, w) {
let columns = new Array(w);
let rows = new Array(h);
for (var i = 0; i < arr.length; i++) {
const word = arr[i];
var {column, depth} = serialToColumnMap(i, h, w);
if (!columns[column]) {
columns[column] = new Array(h);
}
columns[column][depth] = word;
var {row, index} = serialToRowMap(i, h, w);
if (!rows[row]) {
rows[row] = new Array(w);
}
rows[row][index] = word;
}
return {columns, rows}
}
function serialToColumnMap(index, h, w) {
return {
column: index % w,
depth: Math.floor(index / w)
}
}
function serialToRowMap(index, h, w) {
return {
row: Math.floor(index / w),
index: index % w
}
}
function imgDataToWords(arr) {
let result = [];
for (let i = 0; i < arr.length-1; i += 4) {
result.push(`${arr[i]}_${arr[i+1]}_${arr[i+2]}_${arr[i+3]}`)
}
return result;
}
+6
View File
@@ -0,0 +1,6 @@
importScripts('diff.js');
importScripts('diverged.js');
self.addEventListener('message', function(e) {
self.postMessage(diverged(...e.data.divergedInput));
self.close();
}, false);
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Disable Cache -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>BackstopJS Report</title>
<style>
@font-face {
font-family: 'latoregular';
src: url('./assets/fonts/lato-regular-webfont.woff2') format('woff2'),
url('./assets/fonts/lato-regular-webfont.woff') format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'latobold';
src: url('./assets/fonts/lato-bold-webfont.woff2') format('woff2'),
url('./assets/fonts/lato-bold-webfont.woff') format('woff');
font-weight: 700;
font-style: normal;
}
.ReactModal__Body--open {
overflow: hidden;
}
.ReactModal__Body--open .header {
display: none;
}
</style>
</head>
<body style="background-color: #E2E7EA">
<div id="root">
</div>
<script>
function report (report) { // eslint-disable-line no-unused-vars
window.tests = report;
}
</script>
<script src="config.js"></script>
<script src="index_bundle.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
+59
View File
@@ -0,0 +1,59 @@
/*!
Copyright (c) 2015 Jed Watson.
Based on code that is Copyright 2013-2015, Facebook, Inc.
All rights reserved.
*/
/*!
* Adapted from jQuery UI core
*
* http://jqueryui.com
*
* Copyright 2014 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/ui-core/
*/
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
+20
View File
@@ -0,0 +1,20 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"env": {
"browser": true,
"node": false
},
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2
}
}
+97
View File
@@ -0,0 +1,97 @@
export const approveTest = id => {
return {
type: 'APPROVE_TEST',
id
};
};
export const filterTests = status => {
return {
type: 'FILTER_TESTS',
status
};
};
export const findTests = value => {
return {
type: 'SEARCH_TESTS',
value
};
};
export const updateSettings = id => {
return {
type: 'UPDATE_SETTINGS',
id
};
};
export const toggleAllImages = value => {
return {
type: 'TOGGLE_ALL_IMAGES',
value
};
};
export const openModal = value => {
return {
type: 'OPEN_SCRUBBER_MODAL',
value
};
};
export const closeModal = value => {
return {
type: 'CLOSE_SCRUBBER_MODAL',
value
};
};
export const showScrubberTestImage = value => {
return {
type: 'SHOW_SCRUBBER_TEST_IMAGE',
value
};
};
export const showScrubberRefImage = value => {
return {
type: 'SHOW_SCRUBBER_REF_IMAGE',
value
};
};
export const showScrubberDiffImage = value => {
return {
type: 'SHOW_SCRUBBER_DIFF_IMAGE',
value
};
};
export const showScrubberDivergedImage = value => {
return {
type: 'SHOW_SCRUBBER_DIVERGED_IMAGE',
value
};
};
export const showScrubber = value => {
return {
type: 'SHOW_SCRUBBER',
value
};
};
export const openLogModal = value => {
return {
type: 'OPEN_LOG_MODAL',
value
};
};
export const closeLogModal = value => {
return {
type: 'CLOSE_LOG_MODAL',
value
};
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+29
View File
@@ -0,0 +1,29 @@
import React from 'react';
import styled from 'styled-components';
// ESLint
/* eslint-disable no-unused-vars */
import { StickyContainer } from 'react-sticky';
import Header from './ecosystems/Header';
import List from './ecosystems/List';
import ScrubberModal from './ecosystems/ScrubberModal';
import LogModal from './ecosystems/LogModal';
const Wrapper = styled.section`
padding: 0 30px;
`;
export default class App extends React.Component {
render () {
return (
<StickyContainer>
<Header />
<Wrapper>
<List />
</Wrapper>
<ScrubberModal />
<LogModal />
</StickyContainer>
);
}
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import styled from 'styled-components';
import { colors, fonts, shadows } from '../../styles';
const Button = styled.button`
font-size: 20px;
font-family: ${fonts.latoRegular};
flex: 0 0 auto;
margin: 0;
background-color: ${colors.white};
border: none;
border-radius: 3px;
box-shadow: ${props => (props.selected ? 'none' : shadows.shadow01)};
color: ${colors.primaryText};
margin-right: 15px;
padding: 0px 30px;
opacity: ${props => (props.selected ? '1' : '0.5')};
outline: none;
height: 100%;
transition: all 0.3s ease-in-out;
&:hover {
cursor: pointer;
box-shadow: ${props => (!props.selected ? shadows.shadow02 : '')};
}
&.pass {
background-color: ${colors.green};
color: ${colors.white};
}
&.fail {
background-color: ${colors.red};
color: ${colors.white};
}
`;
export default class ButtonFilter extends React.Component {
render () {
const { count, label, status } = this.props;
return (
<Button
onClick={this.props.onClick}
selected={this.props.selected}
className={status}
>
{status !== 'all' ? count : ''} {label}
</Button>
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import React from 'react';
import styled from 'styled-components';
import { colors, fonts, shadows } from '../../styles';
import settingsIcon from '../../assets/icons/settings.png';
const Button = styled.button`
border: none;
height: 100%;
border-radius: 3px;
background: ${colors.lightGray};
margin-left: 15px;
padding: 0 20px;
box-shadow: ${shadows.shadow01};
transition: all 0.3s ease-in-out;
&.active {
box-shadow: none;
opacity: 0.6;
}
&:hover {
cursor: pointer;
box-shadow: ${props => (!props.selected ? shadows.shadow02 : '')};
}
&:focus {
outline: none;
}
.icon {
height: 18px;
width: 18px;
display: block;
background-image: url(${settingsIcon});
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
margin: 0 auto;
padding-bottom: 5px;
}
.label {
font-family: ${fonts.latoRegular};
color: ${colors.secondaryText};
}
`;
export default class ButtonSettings extends React.Component {
render () {
const isActive = this.props.active ? 'active' : '';
return (
<Button onClick={this.props.onClick} className={isActive}>
<span className="icon" />
{/* <span className="label">settings</span> */}
</Button>
);
}
}
+37
View File
@@ -0,0 +1,37 @@
import React from 'react';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
const Label = styled.span`
font-family: ${fonts.latoRegular};
color: ${colors.secondaryText};
font-size: 14px;
padding-right: 8px;
`;
const Value = styled.span`
font-family: ${fonts.latoBold};
color: ${colors.primaryText};
font-size: 14px;
padding-right: 20px;
`;
export default class DiffDetails extends React.Component {
render () {
const { diff, suppress } = this.props;
if (!diff || suppress) {
return null;
}
return (
<span>
<Label>diff%: </Label>
<Value>{diff.misMatchPercentage} </Value>
<Label>diff-x: </Label>
<Value>{diff.dimensionDifference.width} </Value>
<Label>diff-y: </Label>
<Value>{diff.dimensionDifference.height} </Value>
</span>
);
}
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
const DetailsPanel = styled.div`
background: transparent;
display: ${props => (props.display ? 'block' : 'none')};
padding: 10px;
font-family: ${fonts.latoRegular};
color: ${colors.secondaryText};
`;
const ErrorMsg = styled.p`
word-wrap: break-word;
font-family: monospace;
background: rgb(251, 234, 234);
padding: 2ex;
color: brown;
display: ${props => (props.display ? 'block' : 'none')};
`;
class ErrorMessages extends React.Component {
constructor (props) {
super(props);
this.state = {};
}
render () {
const backstopError = this.props.info.error;
const engineError = this.props.info.engineErrorMsg;
const display = !!engineError || !!backstopError;
return (
<DetailsPanel display={display}>
<ErrorMsg display={engineError}>ENGINE ERROR: {engineError}</ErrorMsg>
<ErrorMsg display={backstopError}>
BACKSTOP ERROR: {backstopError}
</ErrorMsg>
</DetailsPanel>
);
}
}
const mapStateToProps = state => {
return {
settings: state.layoutSettings
};
};
const ErrorMessagesContainer = connect(mapStateToProps)(ErrorMessages);
export default ErrorMessagesContainer;
+46
View File
@@ -0,0 +1,46 @@
import React from 'react';
import { connect } from 'react-redux';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
const IdTitle = styled.h3`
font-size: 14px;
font-family: ${fonts.arial};
font-weight: normal;
font-style: normal;
margin: 0;
color: ${colors.secondaryText};
flex: 1 0 auto;
padding-left: 15px;
margin-left: 15px;
margin-top: 7px;
position: relative;
:before {
content: '';
width: 2px;
height: 35px;
background: ${colors.borderGray};
display: block;
position: absolute;
left: 0;
top: -10px;
}
`;
class IdConfig extends React.Component {
render () {
return <IdTitle>{this.props.idConfig}</IdTitle>;
}
}
const mapStateToProps = state => {
return {
idConfig: state.suiteInfo.idConfig
};
};
const IdContainer = connect(mapStateToProps)(IdConfig);
export default IdContainer;
+100
View File
@@ -0,0 +1,100 @@
import React from 'react';
import { connect } from 'react-redux';
import VisibilitySensor from 'react-visibility-sensor';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
const BASE64_PNG_STUB =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const Image = styled.img`
width: auto;
max-width: 100%;
max-height: ${props => (props.settings.textInfo ? '150px' : '400px')};
&:hover {
cursor: pointer;
}
`;
const Wrapper = styled.div`
flex: 1 1 auto;
padding: 0 25px;
padding-top: ${props => (props.withText ? '10px' : '20px')};
text-align: center;
`;
const Label = styled.span`
text-align: center;
font-family: ${fonts.latoRegular};
color: ${colors.secondaryText};
display: block;
margin: 0 auto;
text-transform: uppercase;
padding: 5px 0;
padding-bottom: 15px;
font-size: 12px;
`;
const visibilitySensorProps = {
offset: {
bottom: -400
},
partialVisibility: true
};
class ImagePreview extends React.Component {
constructor (props) {
super(props);
this.state = {
isVisible: false
};
this.onLoadError = this.onLoadError.bind(this);
this.onChange = this.onChange.bind(this);
}
onChange (isVisible) {
if (isVisible && !this.state.isVisible) {
console.log('setting state to visible');
this.setState({
isVisible: true
});
}
}
onLoadError () {
this.setState({
imgLoadError: true
});
}
render () {
let { hidden, settings, label, src } = this.props;
if (!src || src === '../..' || this.state.imgLoadError) {
src = BASE64_PNG_STUB;
}
return (
<VisibilitySensor {...visibilitySensorProps} onChange={this.onChange}>
{this.state.isVisible
? (
<Wrapper hidden={hidden} withText={settings.textInfo}>
<Label>{label}</Label>
<Image {...this.props} src={src} onError={this.onLoadError} />
</Wrapper>
)
: (<p>Most lemurs only see in one or two colors.</p>)
}
</VisibilitySensor>
);
}
}
const mapStateToProps = state => {
return {
settings: state.layoutSettings
};
};
const ImagePreviewContainer = connect(mapStateToProps)(ImagePreview);
export default ImagePreviewContainer;
+325
View File
@@ -0,0 +1,325 @@
import React from 'react';
import styled from 'styled-components';
import TwentyTwenty from 'backstop-twentytwenty';
import { colors, fonts, shadows } from '../../styles';
const ScrubberViewBtn = styled.button`
margin: 1em;
padding: 10px 16px;
height: 32px;
background-color: ${props =>
props.selected ? colors.secondaryText : colors.lightGray};
color: ${props => (props.selected ? colors.lightGray : colors.secondaryText)};
border-radius: 3px;
text-transform: uppercase;
font-family: ${fonts.latoRegular};
text-align: center;
font-size: 12px;
border: none;
box-shadow: ${props => (props.selected ? 'none' : shadows.shadow01)};
transition: all 100ms ease-in-out;
&:focus {
outline: none;
}
&:hover {
cursor: pointer;
box-shadow: ${props => (!props.selected ? shadows.shadow02 : '')};
}
&.loadingDiverged {
animation: blink normal 1200ms infinite ease-in-out;
background-color: green;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0.75;
}
100% {
opacity: 1;
}
}
`;
const Wrapper = styled.div`
cursor: ew-resize;
padding-bottom: 20px;
overflow: hidden;
.testImage {
opacity: 1;
}
.testImage,
.refImage {
max-width: 100%;
}
`;
const WrapTitle = styled.div`
display: flex;
justify-content: center;
padding-top: 10px;
padding-bottom: 10px;
position: sticky;
top: 0;
z-index: 5;
background: white;
border-bottom: 1px solid #e4e4e4;
`;
const SliderBar = styled.div`
height: 100%;
width: 5px;
background: ${colors.red};
transform: translate(-2.5px, 0);
`;
export default class ImageScrubber extends React.Component {
constructor (props) {
super(props);
this.state = {
isRefImageMissing: false,
isLoading: false
};
this.handleRefImageLoadingError = this.handleRefImageLoadingError.bind(this);
this.loadingDiverge = this.loadingDiverge.bind(this);
}
handleRefImageLoadingError () {
this.setState({
isRefImageMissing: true
});
}
loadingDiverge (torf) {
this.setState({
isLoading: !!torf
});
}
render () {
const {
scrubberModalMode,
testImageType,
position,
refImage,
testImage,
diffImage,
divergedImage,
showScrubberTestImage,
showScrubberRefImage,
showScrubberDiffImage,
showScrubberDivergedImage,
showScrubber
} = this.props;
const scrubberTestImageSlug = this.props[testImageType];
const hasDiff = diffImage && diffImage.length > 0;
// only show the diverged option if the report comes from web server
function showDivergedOption () {
return /remote/.test(location.search);
}
// TODO: halp. i don't haz context.
const that = this;
function divergedWorker () {
if (that.state.isLoading) {
console.error('Diverged process is already running. Please hang on.');
return;
}
if (divergedImage) {
showScrubberDivergedImage(divergedImage);
return;
}
showScrubberDivergedImage('');
that.loadingDiverge(true);
const refImg = document.images.isolatedRefImage;
const testImg = document.images.isolatedTestImage;
const h = refImg.height;
const w = refImg.width;
const worker = new Worker('divergedWorker.js');
worker.addEventListener(
'message',
function (result) {
const divergedImgData = result.data;
const clampedImgData = getEmptyImgData(h, w);
for (let i = divergedImgData.length - 1; i >= 0; i--) {
clampedImgData.data[i] = divergedImgData[i];
}
const lcsDiffResult = imageToCanvasContext(null, h, w);
lcsDiffResult.putImageData(clampedImgData, 0, 0);
const divergedImageResult = lcsDiffResult.canvas.toDataURL(
'image/png'
);
showScrubberDivergedImage(divergedImageResult);
that.loadingDiverge(false);
},
false
);
worker.addEventListener('error', function (error) {
showScrubberDivergedImage('');
that.loadingDiverge(false);
console.error(error);
});
worker.postMessage({
divergedInput: [
getImgDataDataFromContext(imageToCanvasContext(refImg)),
getImgDataDataFromContext(imageToCanvasContext(testImg)),
h,
w
]
});
}
const dontUseScrubberView = this.state.isRefImageMissing || !hasDiff;
const showIsolatedRefImage = !hasDiff && scrubberModalMode === 'SHOW_SCRUBBER_REF_IMAGE';
const showIsolatedTestImage = !hasDiff && scrubberModalMode === 'SHOW_SCRUBBER_TEST_IMAGE';
return (
<div>
<WrapTitle>
{hasDiff && (
<div>
<ScrubberViewBtn
selected={scrubberModalMode === 'SHOW_SCRUBBER_REF_IMAGE'}
onClick={showScrubberRefImage}
>
REFERENCE
</ScrubberViewBtn>
<ScrubberViewBtn
selected={scrubberModalMode === 'SHOW_SCRUBBER_TEST_IMAGE'}
onClick={showScrubberTestImage}
>
TEST
</ScrubberViewBtn>
<ScrubberViewBtn
selected={scrubberModalMode === 'SHOW_SCRUBBER_DIFF_IMAGE'}
onClick={showScrubberDiffImage}
>
DIFF
</ScrubberViewBtn>
<ScrubberViewBtn
selected={scrubberModalMode === 'SCRUB'}
onClick={showScrubber}
>
SCRUBBER
</ScrubberViewBtn>
<ScrubberViewBtn
selected={scrubberModalMode === 'SHOW_SCRUBBER_DIVERGED_IMAGE'}
onClick={divergedWorker}
className={this.state.isLoading ? 'loadingDiverged' : ''}
style={{
display: showDivergedOption() ? '' : 'none'
}}
>
{this.state.isLoading ? 'DIVERGING!' : 'DIVERGED'}
</ScrubberViewBtn>
</div>
)}
</WrapTitle>
<Wrapper>
<img
id="isolatedRefImage"
src={refImage}
style={{
margin: 'auto',
display: showIsolatedRefImage ? 'block' : 'none'
}}
/>
<img
id="isolatedTestImage"
className="testImage"
src={testImage}
style={{
margin: 'auto',
display: showIsolatedTestImage ? 'block' : 'none'
}}
/>
<img
className="diffImage"
src={diffImage}
style={{
margin: 'auto',
display: dontUseScrubberView ? 'block' : 'none'
}}
/>
<div
style={{
display: dontUseScrubberView ? 'none' : 'block'
}}
>
<TwentyTwenty
verticalAlign="top"
minDistanceToBeginInteraction={0}
maxAngleToBeginInteraction={Infinity}
initialPosition={position}
newPosition={position}
>
<img
id="scrubberRefImage"
className="refImage"
src={refImage}
onError={this.handleRefImageLoadingError}
/>
<img
id="scrubberTestImage"
className="testImage"
src={scrubberTestImageSlug}
/>
<SliderBar className="slider" />
</TwentyTwenty>
</div>
</Wrapper>
</div>
);
}
}
/**
* ========= DIVERGED HELPERS ========
*/
function getImgDataDataFromContext (context) {
return context.getImageData(0, 0, context.canvas.width, context.canvas.height)
.data;
}
function getEmptyImgData (h, w) {
const o = imageToCanvasContext(null, h, w);
return o.createImageData(w, h);
}
function imageToCanvasContext (_img, h, w) {
let img = _img;
if (!_img) {
img = { height: h, width: w };
}
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const context = canvas.getContext('2d');
if (_img) {
context.drawImage(img, 0, 0);
}
return context;
}
@@ -0,0 +1,46 @@
import React from 'react';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
import searchIcon from '../../assets/icons/search.png';
const Input = styled.input`
display: block;
height: 100%;
border: none;
font-size: 16px;
background-color: ${colors.lightGray};
padding: 0 10px 0 55px;
font-family: ${fonts.latoRegular};
width: 100%;
box-sizing: border-box;
border-radius: 3px;
background-image: url(${searchIcon});
background-repeat: no-repeat;
background-position-x: 15px;
background-position-y: calc(100% / 2);
background-size: 22px;
&:focus {
outline: none;
}
&::placeholder {
font-family: ${fonts.arial};
font-weight: 400;
font-style: italic;
color: ${colors.secondaryText};
}
`;
export default class ButtonFilter extends React.Component {
render () {
return (
<Input
placeholder="Filter tests with search..."
onChange={this.props.onChange.bind(this)}
/>
);
}
}
+78
View File
@@ -0,0 +1,78 @@
import React from 'react';
import styled from 'styled-components';
import { colors, fonts } from '../../styles';
import { connect } from 'react-redux';
import { openLogModal } from '../../actions';
const Label = styled.span`
font-family: ${fonts.latoRegular};
color: ${colors.secondaryText};
font-size: 14px;
padding-right: 8px;
`;
const Value = styled.span`
font-family: ${fonts.latoBold};
color: ${colors.primaryText};
font-size: 14px;
padding-right: 20px;
`;
const Link = styled.a`
&::before {
content: ${props => (props.withSeperator ? '"|"' : '')};
margin: ${props => (props.withSeperator ? '0 10px' : '')};
}
`;
export class LogDetails extends React.Component {
onClick (log, event) {
const { openLogModal } = this.props;
event.preventDefault();
openLogModal(log);
}
render () {
const { referenceLog, testLog } = this.props;
if (!referenceLog && !testLog) return [];
return (
<span>
<Label>browser log: </Label>
<Value>
{
testLog
? <Link href="#" onClick={ this.onClick.bind(this, testLog) } >
test
</Link>
: []
}
{
referenceLog
? <Link withSeperator href="#" onClick={ this.onClick.bind(this, referenceLog) } >
reference
</Link>
: []
}
</Value>
</span>
);
}
}
const mapStateToProps = state => {
return {};
};
const mapDispatchToProps = dispatch => {
return {
openLogModal: value => {
dispatch(openLogModal(value));
}
};
};
const LogDetailsContainer = connect(mapStateToProps, mapDispatchToProps)(
LogDetails
);
export default LogDetailsContainer;

Some files were not shown because too many files have changed in this diff Show More