From 44c1b12d5d37f790886fc355a280703b27a4fe22 Mon Sep 17 00:00:00 2001 From: Roman Kuznetsov Date: Mon, 4 Sep 2023 16:51:23 +0300 Subject: [PATCH] perf: remove immer from static report --- lib/static/modules/reducers/api-values.js | 13 +- .../modules/reducers/bottom-progress-bar.js | 10 +- lib/static/modules/reducers/config.js | 15 +- .../modules/reducers/grouped-tests/by/meta.js | 16 +- .../reducers/grouped-tests/by/result.js | 15 +- .../modules/reducers/grouped-tests/helpers.js | 8 +- .../modules/reducers/grouped-tests/index.js | 37 +- lib/static/modules/reducers/tree/helpers.js | 17 +- lib/static/modules/reducers/tree/index.js | 322 +++++++++--------- .../modules/reducers/tree/nodes/browsers.js | 43 ++- .../modules/reducers/tree/nodes/images.js | 19 +- .../modules/reducers/tree/nodes/results.js | 11 +- .../modules/reducers/tree/nodes/suites.js | 112 +++--- lib/static/modules/utils.js | 51 ++- lib/static/modules/utils/index.js | 182 ++++++++++ lib/static/modules/utils/state.js | 60 ++++ .../modules/reducers/grouped-tests/by/meta.js | 4 +- .../reducers/grouped-tests/by/result.js | 12 +- .../modules/reducers/grouped-tests/index.js | 4 +- 19 files changed, 644 insertions(+), 307 deletions(-) create mode 100644 lib/static/modules/utils/index.js create mode 100644 lib/static/modules/utils/state.js diff --git a/lib/static/modules/reducers/api-values.js b/lib/static/modules/reducers/api-values.js index a91ad223e..3cafc36af 100644 --- a/lib/static/modules/reducers/api-values.js +++ b/lib/static/modules/reducers/api-values.js @@ -1,4 +1,5 @@ import actionNames from '../action-names'; +import {applyStateUpdate} from '../utils/state'; export default (state, action) => { switch (action.type) { @@ -6,20 +7,10 @@ export default (state, action) => { case actionNames.INIT_STATIC_REPORT: { const {apiValues} = action.payload; - return applyChanges(state, apiValues); + return applyStateUpdate(state, {apiValues}); } default: return state; } }; - -function applyChanges(state, apiValues) { - return { - ...state, - apiValues: { - ...state.apiValues, - ...apiValues - } - }; -} diff --git a/lib/static/modules/reducers/bottom-progress-bar.js b/lib/static/modules/reducers/bottom-progress-bar.js index 943bc1f59..2041238c7 100644 --- a/lib/static/modules/reducers/bottom-progress-bar.js +++ b/lib/static/modules/reducers/bottom-progress-bar.js @@ -1,16 +1,14 @@ import actionNames from '../action-names'; -import {produce} from 'immer'; -import {set} from 'lodash'; +import {applyStateUpdate} from '../utils/state'; -export default produce((draft, action) => { +export default ((state, action) => { switch (action.type) { case actionNames.UPDATE_BOTTOM_PROGRESS_BAR: { const {currentRootSuiteId} = action.payload; - - return set(draft, 'progressBar.currentRootSuiteId', currentRootSuiteId); + return applyStateUpdate(state, {progressBar: {currentRootSuiteId}}); } default: - return draft; + return state; } }); diff --git a/lib/static/modules/reducers/config.js b/lib/static/modules/reducers/config.js index 91a5b7477..e6d465327 100644 --- a/lib/static/modules/reducers/config.js +++ b/lib/static/modules/reducers/config.js @@ -1,6 +1,7 @@ import {cloneDeep} from 'lodash'; import {CONTROL_TYPE_RADIOBUTTON} from '../../../gui/constants/custom-gui-control-types'; import actionNames from '../action-names'; +import {applyStateUpdate} from '../utils/state'; export default (state, action) => { switch (action.type) { @@ -10,7 +11,7 @@ export default (state, action) => { config.errorPatterns = formatErrorPatterns(config.errorPatterns); - return applyChanges(state, config); + return applyStateUpdate(state, {config}); } case actionNames.RUN_CUSTOM_GUI_ACTION: { @@ -22,7 +23,7 @@ export default (state, action) => { if (type === CONTROL_TYPE_RADIOBUTTON) { controls.forEach((control, i) => control.active = (controlIndex === i)); - return applyChanges(state, {customGui}); + return applyStateUpdate(state, {config: {customGui}}); } return state; @@ -36,13 +37,3 @@ export default (state, action) => { function formatErrorPatterns(errorPatterns) { return errorPatterns.map((patternInfo) => ({...patternInfo, regexp: new RegExp(patternInfo.pattern)})); } - -function applyChanges(state, config) { - return { - ...state, - config: { - ...state.config, - ...config - } - }; -} diff --git a/lib/static/modules/reducers/grouped-tests/by/meta.js b/lib/static/modules/reducers/grouped-tests/by/meta.js index cc1ace274..1b3fbc007 100644 --- a/lib/static/modules/reducers/grouped-tests/by/meta.js +++ b/lib/static/modules/reducers/grouped-tests/by/meta.js @@ -1,9 +1,10 @@ import {handleActiveResults, addGroupItem, sortGroupValues} from '../helpers'; +import {ensureDiffProperty} from '../../../utils/state'; -export function groupMeta({group, groupKey, ...restArgs}) { +export function groupMeta({group, groupKey, diff = group, ...restArgs}) { const metaKeys = new Set(); if (groupKey) { - group.byKey[groupKey] = {}; + ensureDiffProperty(diff, ['byKey', groupKey]); } const resultCb = (result) => { @@ -18,14 +19,19 @@ export function groupMeta({group, groupKey, ...restArgs}) { continue; } - addGroupItem({group: group.byKey[groupKey], result, value}); + addGroupItem({ + group: group.byKey[groupKey], + result, + value, + diff: diff.byKey[groupKey] + }); } }; handleActiveResults({...restArgs, resultCb}); - group.allKeys = [...metaKeys].sort(); + diff.allKeys = [...metaKeys].sort(); if (groupKey) { - group.byKey[groupKey] = sortGroupValues(group.byKey[groupKey]); + diff.byKey[groupKey] = sortGroupValues(diff.byKey[groupKey]); } } diff --git a/lib/static/modules/reducers/grouped-tests/by/result.js b/lib/static/modules/reducers/grouped-tests/by/result.js index 9c0d19665..bda822228 100644 --- a/lib/static/modules/reducers/grouped-tests/by/result.js +++ b/lib/static/modules/reducers/grouped-tests/by/result.js @@ -1,6 +1,7 @@ import {get} from 'lodash'; import {handleActiveResults, addGroupItem, sortGroupValues} from '../helpers'; import {isAssertViewError} from '../../../utils'; +import {ensureDiffProperty} from '../../../utils/state'; import {ERROR_KEY, RESULT_KEYS} from '../../../../../constants/group-tests'; const imageComparisonErrorMessage = 'image comparison failed'; @@ -13,21 +14,27 @@ export function groupResult(args) { throw new Error(`Group key must be one of ${RESULT_KEYS.join(', ')}, but got ${args.groupKey}`); } -function groupErrors({tree, group, groupKey, errorPatterns = [], ...viewArgs}) { - group.byKey[groupKey] = {}; +function groupErrors({tree, group, groupKey, errorPatterns = [], diff = group, ...viewArgs}) { + ensureDiffProperty(diff, ['byKey', groupKey]); const resultCb = (result) => { const images = result.imageIds.map((imageId) => tree.images.byId[imageId]); const errors = extractErrors(result, images); for (const errorText of errors) { - addGroupItem({group: group.byKey[groupKey], result, value: errorText, patterns: errorPatterns}); + addGroupItem({ + group: group.byKey[groupKey], + result, + value: errorText, + patterns: errorPatterns, + diff: diff.byKey[groupKey] + }); } }; handleActiveResults({tree, ...viewArgs, resultCb}); - group.byKey[groupKey] = sortGroupValues(group.byKey[groupKey]); + diff.byKey[groupKey] = sortGroupValues(diff.byKey[groupKey]); } function extractErrors(result, images) { diff --git a/lib/static/modules/reducers/grouped-tests/helpers.js b/lib/static/modules/reducers/grouped-tests/helpers.js index c5574c5f7..4b2616375 100644 --- a/lib/static/modules/reducers/grouped-tests/helpers.js +++ b/lib/static/modules/reducers/grouped-tests/helpers.js @@ -26,13 +26,13 @@ export function handleActiveResults({tree = {}, viewMode = ViewMode.ALL, filtere } } -export function addGroupItem({group, result, value, patterns = []}) { +export function addGroupItem({group, result, value, patterns = [], diff = group}) { const stringifiedValue = stringify(value); const {pattern, name} = matchGroupWithPatterns(stringifiedValue, patterns); // eslint-disable-next-line no-prototype-builtins - if (!group.hasOwnProperty(name)) { - group[name] = { + if ((!group || !group.hasOwnProperty(name)) && !diff.hasOwnProperty(name)) { + diff[name] = { pattern, name, browserIds: [result.parentId], @@ -44,7 +44,7 @@ export function addGroupItem({group, result, value, patterns = []}) { return; } - const groupItem = group[name]; + const groupItem = diff[name]; if (!groupItem.browserIds.includes(result.parentId)) { groupItem.browserIds.push(result.parentId); diff --git a/lib/static/modules/reducers/grouped-tests/index.js b/lib/static/modules/reducers/grouped-tests/index.js index e065ff149..de2a98cf3 100644 --- a/lib/static/modules/reducers/grouped-tests/index.js +++ b/lib/static/modules/reducers/grouped-tests/index.js @@ -1,11 +1,11 @@ -import {produce} from 'immer'; import actionNames from '../../action-names'; import {groupMeta} from './by/meta'; import {groupResult} from './by/result'; import {SECTIONS} from '../../../../constants/group-tests'; import {parseKeyToGroupTestsBy} from '../../utils'; +import {applyStateUpdate, ensureDiffProperty} from '../../utils/state'; -export default produce((state, action) => { +export default (state, action) => { switch (action.type) { case actionNames.INIT_GUI_REPORT: case actionNames.INIT_STATIC_REPORT: @@ -23,30 +23,51 @@ export default produce((state, action) => { view: {keyToGroupTestsBy, viewMode, filteredBrowsers, testNameFilter, strictMatchFilter} } = state; const viewArgs = {viewMode, filteredBrowsers, testNameFilter, strictMatchFilter}; + const diff = {groupedTests: {meta: {}}}; if (!keyToGroupTestsBy) { - groupMeta({tree, group: groupedTests.meta, ...viewArgs}); + groupMeta({tree, group: groupedTests.meta, diff: diff.groupedTests.meta, ...viewArgs}); - return; + return applyStateUpdate(state, diff); } const [groupSection, groupKey] = parseKeyToGroupTestsBy(keyToGroupTestsBy); + ensureDiffProperty(diff, ['groupedTests', groupSection]); + const group = groupedTests[groupSection]; + const groupDiff = diff.groupedTests[groupSection]; if (groupSection === SECTIONS.RESULT) { const {config: {errorPatterns}} = state; - return groupResult({tree, group, groupKey, errorPatterns, ...viewArgs}); + groupResult({ + tree, + group, + groupKey, + errorPatterns, + diff: groupDiff, + ...viewArgs + }); + + return applyStateUpdate(state, diff); } if (groupSection === SECTIONS.META) { - return groupMeta({tree, group, groupKey, ...viewArgs}); + groupMeta({ + tree, + group, + groupKey, + diff: groupDiff, + ...viewArgs + }); + + return applyStateUpdate(state, diff); } - break; + return state; } default: return state; } -}); +}; diff --git a/lib/static/modules/reducers/tree/helpers.js b/lib/static/modules/reducers/tree/helpers.js index 84e02845d..afcc132bd 100644 --- a/lib/static/modules/reducers/tree/helpers.js +++ b/lib/static/modules/reducers/tree/helpers.js @@ -1,12 +1,13 @@ import {get, set} from 'lodash'; import {EXPAND_ALL, EXPAND_ERRORS, EXPAND_RETRIES} from '../../../../constants/expand-modes'; +import {getUpdatedProperty} from '../../utils/state'; -export function changeNodeState(nodesStateById, nodeId, state) { +export function changeNodeState(nodesStateById, nodeId, state, diff = nodesStateById) { Object.keys(state).forEach((stateName) => { const prevStateValue = get(nodesStateById[nodeId], stateName); if (prevStateValue !== state[stateName]) { - set(nodesStateById, [nodeId, stateName], state[stateName]); + set(diff, [nodeId, stateName], state[stateName]); } }); } @@ -27,17 +28,19 @@ export function shouldNodeBeOpened(expand, {errorsCb, retriesCb}) { return false; } -export function getShownCheckedChildCount(tree, suiteId) { +export function getShownCheckedChildCount(tree, suiteId, diff = tree) { const {suiteIds = [], browserIds = []} = tree.suites.byId[suiteId]; const checkedChildBrowserCount = browserIds.reduce((sum, browserChildId) => { - const browserState = tree.browsers.stateById[browserChildId]; + const shouldBeShown = getUpdatedProperty(tree, diff, ['browsers', 'stateById', browserChildId, 'shouldBeShown']); + const checkStatus = getUpdatedProperty(tree, diff, ['browsers', 'stateById', browserChildId, 'checkStatus']); - return sum + (browserState.shouldBeShown && browserState.checkStatus); + return sum + (shouldBeShown && checkStatus); }, 0); const checkedChildSuitesCount = suiteIds.reduce((sum, suiteChildId) => { - const suiteState = tree.suites.stateById[suiteChildId]; + const shouldBeShown = getUpdatedProperty(tree, diff, ['suites', 'stateById', suiteChildId, 'shouldBeShown']); + const checkStatus = getUpdatedProperty(tree, diff, ['suites', 'stateById', suiteChildId, 'checkStatus']); - return sum + (suiteState.shouldBeShown && suiteState.checkStatus); + return sum + (shouldBeShown && checkStatus); }, 0); return checkedChildBrowserCount + checkedChildSuitesCount; diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index 4ab996e2f..1d02a0aee 100644 --- a/lib/static/modules/reducers/tree/index.js +++ b/lib/static/modules/reducers/tree/index.js @@ -17,80 +17,65 @@ import {ViewMode} from '../../../../constants/view-modes'; import {EXPAND_RETRIES} from '../../../../constants/expand-modes'; import {FAIL} from '../../../../constants/test-statuses'; import {isSuccessStatus} from '../../../../common-utils'; +import {applyStateUpdate, ensureDiffProperty, getUpdatedProperty} from '../../utils/state'; + +export default ((state, action) => { + const diff = {tree: {}}; -export default produce((state, action) => { switch (action.type) { case actionNames.INIT_GUI_REPORT: case actionNames.INIT_STATIC_REPORT: { const {tree} = action.payload; const {filteredBrowsers} = state.view; - state.tree = tree; - state.tree.suites.failedRootIds = getFailedRootSuiteIds(tree.suites); + tree.suites.failedRootIds = getFailedRootSuiteIds(tree.suites); - state.tree.suites.stateById = {}; - state.tree.browsers.stateById = {}; - state.tree.results.stateById = {}; - state.tree.images.stateById = {}; + tree.suites.stateById = {}; + tree.browsers.stateById = {}; + tree.results.stateById = {}; + tree.images.stateById = {}; - updateAllSuitesStatus(state.tree, filteredBrowsers); - initNodesStates(state); + updateAllSuitesStatus(tree, filteredBrowsers); + initNodesStates({tree, view: state.view}); - break; + return {...state, tree}; } case actionNames.RUN_ALL_TESTS: { const {status} = action.payload; const {tree} = state; - tree.suites.allIds.forEach((suiteId) => { - tree.suites.byId[suiteId].status = status; - }); - - break; - } + ensureDiffProperty(diff, ['tree', 'suites', 'byId']); - case actionNames.TEST_BEGIN: { - [].concat(action.payload).forEach(({result, suites}) => { - addResult(state.tree, result); - setBrowsersLastRetry(state.tree, result.parentId); - updateSuitesStatus(state.tree, suites); + tree.suites.allIds.forEach((suiteId) => { + diff.tree.suites.byId[suiteId] = {status}; }); - break; - } - - case actionNames.TEST_RESULT: - case actionNames.ACCEPT_OPENED_SCREENSHOTS: - case actionNames.ACCEPT_SCREENSHOT: - case actionNames.APPLY_DELAYED_TEST_RESULTS: { - addNodesToTree(state, action.payload); - - break; + return applyStateUpdate(state, diff); } case actionNames.TOGGLE_SUITE_SECTION: { const {suiteId, shouldBeOpened} = action.payload; - changeSuiteState(state.tree, suiteId, {shouldBeOpened}); + changeSuiteState(state.tree, suiteId, {shouldBeOpened}, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.TOGGLE_BROWSER_SECTION: { const {browserId, shouldBeOpened} = action.payload; - changeBrowserState(state.tree, browserId, {shouldBeOpened}); + changeBrowserState(state.tree, browserId, {shouldBeOpened}, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.TOGGLE_STATE_RESULT: { const {imageId, shouldBeOpened} = action.payload; - changeImageState(state.tree, imageId, {shouldBeOpened}); + changeImageState(state.tree, imageId, {shouldBeOpened}, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.CHANGE_TEST_RETRY: { @@ -101,9 +86,9 @@ export default produce((state, action) => { browserState.lastMatchedRetryIndex = null; } - changeBrowserState(state.tree, browserId, browserState); + changeBrowserState(state.tree, browserId, browserState, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.BROWSERS_SELECTED: { @@ -112,11 +97,11 @@ export default produce((state, action) => { ? state.browsers.map(({id}) => ({id, versions: []})) : view.filteredBrowsers; - updateAllSuitesStatus(tree, filteredBrowsers); - calcBrowsersShowness(tree, view); - calcSuitesShowness(tree); + updateAllSuitesStatus(tree, filteredBrowsers, diff.tree); + calcBrowsersShowness({tree, view, diff: diff.tree}); + calcSuitesShowness({tree, diff: diff.tree}); - break; + return applyStateUpdate(state, diff); } case actionNames.CHANGE_VIEW_MODE: @@ -124,43 +109,43 @@ export default produce((state, action) => { case actionNames.VIEW_SET_STRICT_MATCH_FILTER: { const {tree, view} = state; - calcBrowsersShowness(tree, view); - calcSuitesShowness(tree); + calcBrowsersShowness({tree, view, diff: diff.tree}); + calcSuitesShowness({tree, diff: diff.tree}); - break; + return applyStateUpdate(state, diff); } case actionNames.VIEW_EXPAND_ALL: { - changeAllNodesState(state.tree, {shouldBeOpened: true}); + changeAllNodesState(state.tree, {shouldBeOpened: true}, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.VIEW_COLLAPSE_ALL: { - changeAllNodesState(state.tree, {shouldBeOpened: false}); + changeAllNodesState(state.tree, {shouldBeOpened: false}, diff.tree); - break; + return applyStateUpdate(state, diff); } case actionNames.VIEW_EXPAND_ERRORS: case actionNames.VIEW_EXPAND_RETRIES: { const {tree, view} = state; - calcSuitesOpenness(tree, view.expand); - calcBrowsersOpenness(tree, view.expand); - calcImagesOpenness(tree, view.expand); + calcSuitesOpenness({tree, expand: view.expand, diff: diff.tree}); + calcBrowsersOpenness({tree, expand: view.expand, diff: diff.tree}); + calcImagesOpenness({tree, expand: view.expand, diff: diff.tree}); - break; + return applyStateUpdate(state, diff); } case actionNames.CLOSE_SECTIONS: { const closeImageIds = action.payload; closeImageIds.forEach((imageId) => { - changeImageState(state.tree, imageId, {shouldBeOpened: false}); + changeImageState(state.tree, imageId, {shouldBeOpened: false}, diff.tree); }); - break; + return applyStateUpdate(state, diff); } case actionNames.TOGGLE_TESTS_GROUP: { @@ -168,24 +153,24 @@ export default produce((state, action) => { const {tree, view} = state; if (!isActive) { - changeAllBrowsersState(tree, {shouldBeShown: false, lastMatchedRetryIndex: null}); - changeAllSuitesState(tree, {shouldBeShown: false}); - changeAllResultsState(tree, {matchedSelectedGroup: false}); + changeAllBrowsersState(tree, {shouldBeShown: false, lastMatchedRetryIndex: null}, diff.tree); + changeAllSuitesState(tree, {shouldBeShown: false}, diff.tree); + changeAllResultsState(tree, {matchedSelectedGroup: false}, diff.tree); - return; + return applyStateUpdate(state, diff); } - calcBrowsersShowness(tree, view, browserIds); + calcBrowsersShowness({tree, view, browserIds, diff: diff.tree}); tree.browsers.allIds.forEach((browserId) => { - const {shouldBeShown} = tree.browsers.stateById[browserId]; + const shouldBeShown = getUpdatedProperty(tree, diff.tree, ['browsers', 'stateById', browserId, 'shouldBeShown']); if (!shouldBeShown) { return; } if (!browserIds.includes(browserId)) { - return changeBrowserState(tree, browserId, {shouldBeShown: false}); + return changeBrowserState(tree, browserId, {shouldBeShown: false}, diff.tree); } const broResultIds = tree.browsers.byId[browserId].resultIds; @@ -193,140 +178,163 @@ export default produce((state, action) => { broResultIds.forEach((resultId, ind) => { if (!resultIds.includes(resultId)) { - changeResultState(tree, resultId, {matchedSelectedGroup: false}); + changeResultState({tree, resultId, state: {matchedSelectedGroup: false}, diff: diff.tree}); return; } lastMatchedRetryIndex = ind; - changeResultState(tree, resultId, {matchedSelectedGroup: true}); + changeResultState({tree, resultId, state: {matchedSelectedGroup: true}, diff: diff.tree}); }); - changeBrowserState(tree, browserId, {lastMatchedRetryIndex}); + changeBrowserState(tree, browserId, {lastMatchedRetryIndex}, diff.tree); }); - calcSuitesShowness(tree); + calcSuitesShowness({tree, diff: diff.tree}); - break; + return applyStateUpdate(state, diff); } case actionNames.GROUP_TESTS_BY_KEY: { const {tree, view} = state; - changeAllResultsState(tree, {matchedSelectedGroup: false}); + changeAllResultsState(tree, {matchedSelectedGroup: false}, diff.tree); if (view.keyToGroupTestsBy) { - changeAllBrowsersState(tree, {shouldBeShown: false, lastMatchedRetryIndex: null}); - changeAllSuitesState(tree, {shouldBeShown: false}); + changeAllBrowsersState(tree, {shouldBeShown: false, lastMatchedRetryIndex: null}, diff.tree); + changeAllSuitesState(tree, {shouldBeShown: false}, diff.tree); - return; + return applyStateUpdate(state, diff); } - calcBrowsersShowness(tree, view); - calcSuitesShowness(tree); - changeAllBrowsersState(tree, {lastMatchedRetryIndex: null}); + calcBrowsersShowness({tree, view, diff: diff.tree}); + calcSuitesShowness({tree, diff: diff.tree}); + changeAllBrowsersState(tree, {lastMatchedRetryIndex: null}, diff.tree); - break; + return applyStateUpdate(state, diff); } + } - case actionNames.UNDO_ACCEPT_IMAGES: { - const {tree, view} = state; - const {updatedImages = [], removedResults = [], skipTreeUpdate} = action.payload; + return produce(state, (draft) => { + switch (action.type) { + case actionNames.TEST_BEGIN: { + [].concat(action.payload).forEach(({result, suites}) => { + addResult(draft.tree, result); + setBrowsersLastRetry(draft.tree, result.parentId); + updateSuitesStatus(draft.tree, suites); + }); - if (skipTreeUpdate) { - return; + break; } - const failedRemovedResults = removedResults.filter(resultId => { - const result = tree.results.byId[resultId]; - const browser = tree.browsers.byId[result.parentId]; - const lastResultId = findLast(browser.resultIds, id => id !== resultId); - const lastResult = tree.results.byId[lastResultId]; - - return !isSuccessStatus(lastResult.status); - }); - const failedResultIds = updatedImages.map(({parentId: resultId}) => resultId).concat(failedRemovedResults); - const suiteIdsToFail = failedResultIds.map(resultId => { - const result = tree.results.byId[resultId]; - const browser = tree.browsers.byId[result.parentId]; - const suite = tree.suites.byId[browser.parentId]; + case actionNames.TEST_RESULT: + case actionNames.ACCEPT_OPENED_SCREENSHOTS: + case actionNames.ACCEPT_SCREENSHOT: + case actionNames.APPLY_DELAYED_TEST_RESULTS: { + addNodesToTree(draft, action.payload); - return suite.id; - }); + break; + } - for (const updatedImage of updatedImages) { - tree.images.byId[updatedImage.id] = updatedImage; - tree.images.stateById[updatedImage.id].shouldBeOpened = true; + case actionNames.UNDO_ACCEPT_IMAGES: { + const {tree, view} = draft; + const {updatedImages = [], removedResults = [], skipTreeUpdate} = action.payload; - tree.results.byId[updatedImage.parentId].status = FAIL; - } + if (skipTreeUpdate) { + return; + } - for (const removedResultId of removedResults) { - removeResult(tree, removedResultId); - } + const failedRemovedResults = removedResults.filter(resultId => { + const result = tree.results.byId[resultId]; + const browser = tree.browsers.byId[result.parentId]; + const lastResultId = findLast(browser.resultIds, id => id !== resultId); + const lastResult = tree.results.byId[lastResultId]; - failSuites(tree, suiteIdsToFail); + return !isSuccessStatus(lastResult.status); + }); + const failedResultIds = updatedImages.map(({parentId: resultId}) => resultId).concat(failedRemovedResults); + const suiteIdsToFail = failedResultIds.map(resultId => { + const result = tree.results.byId[resultId]; + const browser = tree.browsers.byId[result.parentId]; + const suite = tree.suites.byId[browser.parentId]; - calcBrowsersShowness(tree, view); - calcSuitesShowness(tree); + return suite.id; + }); - break; - } + for (const updatedImage of updatedImages) { + tree.images.byId[updatedImage.id] = updatedImage; + tree.images.stateById[updatedImage.id].shouldBeOpened = true; - case actionNames.TOGGLE_BROWSER_CHECKBOX: { - const {suiteBrowserId, checkStatus} = action.payload; - const parentId = getBrowserParentId(state.tree, suiteBrowserId); + tree.results.byId[updatedImage.parentId].status = FAIL; + } - changeBrowserState(state.tree, suiteBrowserId, {checkStatus}); + for (const removedResultId of removedResults) { + removeResult(tree, removedResultId); + } - updateParentsChecked(state.tree, parentId); + failSuites(tree, suiteIdsToFail); - break; - } + calcBrowsersShowness({tree, view}); + calcSuitesShowness({tree}); - case actionNames.TOGGLE_SUITE_CHECKBOX: { - const {suiteId, checkStatus} = action.payload; - const parentId = state.tree.suites.byId[suiteId].parentId; - const toggledSuiteIds = [suiteId]; + break; + } - changeSuiteState(state.tree, suiteId, {checkStatus}); + case actionNames.TOGGLE_BROWSER_CHECKBOX: { + const {suiteBrowserId, checkStatus} = action.payload; + const parentId = getBrowserParentId(draft.tree, suiteBrowserId); - while (toggledSuiteIds.length) { - const suiteCurrId = toggledSuiteIds.pop(); - const suiteChildIds = state.tree.suites.byId[suiteCurrId].suiteIds || []; - const suiteBrowserIds = state.tree.suites.byId[suiteCurrId].browserIds || []; + changeBrowserState(draft.tree, suiteBrowserId, {checkStatus}); - suiteChildIds.forEach(suiteChildId => { - const isSuiteShown = state.tree.suites.stateById[suiteChildId].shouldBeShown; - const newCheckStatus = Number(isSuiteShown && checkStatus); - changeSuiteState(state.tree, suiteChildId, {checkStatus: newCheckStatus}); - }); - suiteBrowserIds.forEach(suiteBrowserId => { - const isBrowserShown = state.tree.browsers.stateById[suiteBrowserId].shouldBeShown; - const newCheckStatus = Number(isBrowserShown && checkStatus); - changeBrowserState(state.tree, suiteBrowserId, {checkStatus: newCheckStatus}); - }); + updateParentsChecked(draft.tree, parentId); - toggledSuiteIds.push(...suiteChildIds); + break; } - updateParentsChecked(state.tree, parentId); + case actionNames.TOGGLE_SUITE_CHECKBOX: { + const {suiteId, checkStatus} = action.payload; + const parentId = draft.tree.suites.byId[suiteId].parentId; + const toggledSuiteIds = [suiteId]; + + changeSuiteState(draft.tree, suiteId, {checkStatus}); + + while (toggledSuiteIds.length) { + const suiteCurrId = toggledSuiteIds.pop(); + const suiteChildIds = draft.tree.suites.byId[suiteCurrId].suiteIds || []; + const suiteBrowserIds = draft.tree.suites.byId[suiteCurrId].browserIds || []; + + suiteChildIds.forEach(suiteChildId => { + const isSuiteShown = draft.tree.suites.stateById[suiteChildId].shouldBeShown; + const newCheckStatus = Number(isSuiteShown && checkStatus); + changeSuiteState(draft.tree, suiteChildId, {checkStatus: newCheckStatus}); + }); + suiteBrowserIds.forEach(suiteBrowserId => { + const isBrowserShown = draft.tree.browsers.stateById[suiteBrowserId].shouldBeShown; + const newCheckStatus = Number(isBrowserShown && checkStatus); + changeBrowserState(draft.tree, suiteBrowserId, {checkStatus: newCheckStatus}); + }); + + toggledSuiteIds.push(...suiteChildIds); + } - break; - } + updateParentsChecked(draft.tree, parentId); - case actionNames.TOGGLE_GROUP_CHECKBOX: { - const {browserIds, checkStatus} = action.payload; - const parentIds = browserIds.map(browserId => getBrowserParentId(state.tree, browserId)); + break; + } - browserIds.forEach(browserId => { - changeBrowserState(state.tree, browserId, {checkStatus}); - }); + case actionNames.TOGGLE_GROUP_CHECKBOX: { + const {browserIds, checkStatus} = action.payload; + const parentIds = browserIds.map(browserId => getBrowserParentId(draft.tree, browserId)); - updateParentsChecked(state.tree, parentIds); + browserIds.forEach(browserId => { + changeBrowserState(draft.tree, browserId, {checkStatus}); + }); + + updateParentsChecked(draft.tree, parentIds); - break; + break; + } } - } + }); }); function initNodesStates(state) { @@ -352,21 +360,21 @@ function addNodesToTree(state, payload) { const suiteIds = view.expand === EXPAND_RETRIES ? youngestSuiteId : suites.map(({id}) => id); const imageIds = images.map(({id}) => id); - calcSuitesOpenness(tree, view.expand, suiteIds); - calcBrowsersOpenness(tree, view.expand, browserId); - calcImagesOpenness(tree, view.expand, imageIds); + calcSuitesOpenness({tree, expand: view.expand, suiteIds}); + calcBrowsersOpenness({tree, expand: view.expand, browserId}); + calcImagesOpenness({tree, expand: view.expand, imageIds}); if (view.viewMode === ViewMode.FAILED) { - calcBrowsersShowness(tree, view, browserId); - calcSuitesShowness(tree, youngestSuiteId); + calcBrowsersShowness({tree, view, browserIds: [browserId]}); + calcSuitesShowness({tree, suiteIds: youngestSuiteId}); } }); tree.suites.failedRootIds = getFailedRootSuiteIds(tree.suites); } -function changeAllNodesState(tree, state) { - changeAllSuitesState(tree, state); - changeAllBrowsersState(tree, state); - changeAllImagesState(tree, state); +function changeAllNodesState(tree, state, diff = tree) { + changeAllSuitesState(tree, state, diff); + changeAllBrowsersState(tree, state, diff); + changeAllImagesState(tree, state, diff); } diff --git a/lib/static/modules/reducers/tree/nodes/browsers.js b/lib/static/modules/reducers/tree/nodes/browsers.js index d66e33bef..4ce363347 100644 --- a/lib/static/modules/reducers/tree/nodes/browsers.js +++ b/lib/static/modules/reducers/tree/nodes/browsers.js @@ -1,5 +1,6 @@ import {isEmpty, last, initial} from 'lodash'; import {isBrowserMatchViewMode, isTestNameMatchFilters, shouldShowBrowser} from '../../../utils'; +import {ensureDiffProperty, getUpdatedProperty} from '../../../utils/state'; import {UNCHECKED} from '../../../../../constants/checked-statuses'; import {isNodeFailed} from '../../../utils'; import {changeNodeState, shouldNodeBeOpened} from '../helpers'; @@ -12,10 +13,10 @@ export function initBrowsersState(tree, view) { if (view.keyToGroupTestsBy) { changeBrowserState(tree, browserId, {shouldBeShown: false}); } else { - calcBrowsersShowness(tree, view, browserId); + calcBrowsersShowness({tree, view, browserIds: [browserId]}); } - calcBrowsersOpenness(tree, view.expand, browserId); + calcBrowsersOpenness({tree, expand: view.expand, browserIds: [browserId]}); }); } @@ -23,14 +24,16 @@ export function getBrowserParentId(tree, browserId) { return tree.browsers.byId[browserId].parentId; } -export function changeAllBrowsersState(tree, state) { +export function changeAllBrowsersState(tree, state, diff = tree) { tree.browsers.allIds.forEach((browserId) => { - changeBrowserState(tree, browserId, state); + changeBrowserState(tree, browserId, state, diff); }); } -export function changeBrowserState(tree, browserId, state) { - changeNodeState(tree.browsers.stateById, browserId, state); +export function changeBrowserState(tree, browserId, state, diff = tree) { + ensureDiffProperty(diff, ['browsers', 'stateById']); + + changeNodeState(tree.browsers.stateById, browserId, state, diff.browsers.stateById); } export function setBrowsersLastRetry(tree, browserIds) { @@ -44,32 +47,36 @@ export function setBrowsersLastRetry(tree, browserIds) { }); } -export function calcBrowsersShowness(tree, view, browserIds) { +export function calcBrowsersShowness({tree, view, browserIds = [], diff = tree}) { + ensureDiffProperty(diff, ['browsers', 'byId']); if (isEmpty(browserIds)) { browserIds = tree.browsers.allIds; } - [].concat(browserIds).forEach((browserId) => { + browserIds.forEach((browserId) => { + ensureDiffProperty(diff.browsers.byId, [browserId]); const browser = tree.browsers.byId[browserId]; - const lastResult = tree.results.byId[last(browser.resultIds)]; - const shouldBeShown = calcBrowserShowness(browser, lastResult, view); + const diffBrowser = diff.browsers.byId[browserId]; + const resultIds = getUpdatedProperty(browser, diffBrowser, 'resultIds'); + const lastResult = tree.results.byId[last(resultIds)]; + const shouldBeShown = calcBrowserShowness(browser, lastResult, view, diffBrowser); const checkStatus = shouldBeShown && tree.browsers.stateById[browserId].checkStatus; - changeBrowserState(tree, browserId, {shouldBeShown, checkStatus}); + changeBrowserState(tree, browserId, {shouldBeShown, checkStatus}, diff); }); } -export function calcBrowsersOpenness(tree, expand, browserIds) { +export function calcBrowsersOpenness({tree, expand, browserIds = [], diff = tree}) { if (isEmpty(browserIds)) { browserIds = tree.browsers.allIds; } - [].concat(browserIds).forEach((browserId) => { + browserIds.forEach((browserId) => { const browser = tree.browsers.byId[browserId]; const lastResult = tree.results.byId[last(browser.resultIds)]; const shouldBeOpened = calcBrowserOpenness(browser, lastResult, expand, tree); - changeBrowserState(tree, browserId, {shouldBeOpened}); + changeBrowserState(tree, browserId, {shouldBeOpened}, diff); }); } @@ -99,18 +106,18 @@ function calcBrowserOpenness(browser, lastResult, expand, tree) { return shouldNodeBeOpened(expand, {errorsCb, retriesCb}); } -function calcBrowserShowness(browser, lastResult, view) { +function calcBrowserShowness(browser, lastResult, view, diff = browser) { const {viewMode, filteredBrowsers, testNameFilter, strictMatchFilter} = view; - if (!isBrowserMatchViewMode(browser, lastResult, viewMode)) { + if (!isBrowserMatchViewMode(browser, lastResult, viewMode, diff)) { return false; } - const testName = browser.parentId; + const testName = getUpdatedProperty(browser, diff, 'parentId'); if (!isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter)) { return false; } - return shouldShowBrowser(browser, filteredBrowsers); + return shouldShowBrowser(browser, filteredBrowsers, diff); } diff --git a/lib/static/modules/reducers/tree/nodes/images.js b/lib/static/modules/reducers/tree/nodes/images.js index b1da125f4..5ea3ec9fa 100644 --- a/lib/static/modules/reducers/tree/nodes/images.js +++ b/lib/static/modules/reducers/tree/nodes/images.js @@ -1,32 +1,35 @@ import _ from 'lodash'; import {isNodeFailed} from '../../../utils'; +import {ensureDiffProperty} from '../../../utils/state'; import {changeNodeState, shouldNodeBeOpened} from '../helpers'; export function initImagesState(tree, expand) { - calcImagesOpenness(tree, expand); + calcImagesOpenness({tree, expand}); } -export function calcImagesOpenness(tree, expand, imageIds) { +export function calcImagesOpenness({tree, expand, imageIds = [], diff = tree}) { if (_.isEmpty(imageIds)) { imageIds = tree.images.allIds; } - [].concat(imageIds).forEach((imageId) => { + imageIds.forEach((imageId) => { const image = tree.images.byId[imageId]; const shouldBeOpened = calcImageOpenness(image, expand); - changeImageState(tree, imageId, {shouldBeOpened}); + changeImageState(tree, imageId, {shouldBeOpened}, diff); }); } -export function changeAllImagesState(tree, state) { +export function changeAllImagesState(tree, state, diff = tree) { tree.images.allIds.forEach((imageId) => { - changeImageState(tree, imageId, state); + changeImageState(tree, imageId, state, diff); }); } -export function changeImageState(tree, imageId, state) { - changeNodeState(tree.images.stateById, imageId, state); +export function changeImageState(tree, imageId, state, diff = tree) { + ensureDiffProperty(diff, ['images', 'stateById']); + + changeNodeState(tree.images.stateById, imageId, state, diff.images.stateById); } export function addImages(tree, images, expand) { diff --git a/lib/static/modules/reducers/tree/nodes/results.js b/lib/static/modules/reducers/tree/nodes/results.js index 5075d0aaf..819038849 100644 --- a/lib/static/modules/reducers/tree/nodes/results.js +++ b/lib/static/modules/reducers/tree/nodes/results.js @@ -1,19 +1,22 @@ import {changeNodeState} from '../helpers'; import {removeResultFromBrowsers} from './browsers'; import {removeImages} from './images'; +import {ensureDiffProperty} from '../../../utils/state'; export function initResultsState(tree) { changeAllResultsState(tree, {matchedSelectedGroup: false}); } -export function changeAllResultsState(tree, state) { +export function changeAllResultsState(tree, state, diff = tree) { tree.results.allIds.forEach((resultId) => { - changeResultState(tree, resultId, state); + changeResultState({tree, resultId, state, diff}); }); } -export function changeResultState(tree, resultId, state) { - changeNodeState(tree.results.stateById, resultId, state); +export function changeResultState({tree, resultId, state, diff = tree}) { + ensureDiffProperty(diff, ['results', 'stateById']); + + changeNodeState(tree.results.stateById, resultId, state, diff.results.stateById); } export function addResult(tree, result) { diff --git a/lib/static/modules/reducers/tree/nodes/suites.js b/lib/static/modules/reducers/tree/nodes/suites.js index 1eadb4cbc..6f252d5cf 100644 --- a/lib/static/modules/reducers/tree/nodes/suites.js +++ b/lib/static/modules/reducers/tree/nodes/suites.js @@ -1,6 +1,7 @@ import _ from 'lodash'; import {getSuiteResults} from '../../../selectors/tree'; import {isNodeFailed} from '../../../utils'; +import {ensureDiffProperty, getUpdatedProperty} from '../../../utils/state'; import {determineStatus, isFailStatus} from '../../../../../common-utils'; import {changeNodeState, getShownCheckedChildCount, shouldNodeBeOpened} from '../helpers'; import {EXPAND_RETRIES} from '../../../../../constants/expand-modes'; @@ -13,20 +14,22 @@ export function initSuitesState(tree, view) { if (view.keyToGroupTestsBy) { changeAllSuitesState(tree, {shouldBeShown: false}); } else { - calcSuitesShowness(tree); + calcSuitesShowness({tree}); } - calcSuitesOpenness(tree, view.expand); + calcSuitesOpenness({tree, expand: view.expand}); } -export function changeAllSuitesState(tree, state) { +export function changeAllSuitesState(tree, state, diff = tree) { tree.suites.allIds.forEach((suiteId) => { - changeSuiteState(tree, suiteId, state); + changeSuiteState(tree, suiteId, state, diff); }); } -export function changeSuiteState(tree, suiteId, state) { - changeNodeState(tree.suites.stateById, suiteId, state); +export function changeSuiteState(tree, suiteId, state, diff = tree) { + ensureDiffProperty(diff, ['suites', 'stateById']); + + changeNodeState(tree.suites.stateById, suiteId, state, diff.suites.stateById); } export function updateSuitesStatus(tree, suites) { @@ -37,13 +40,14 @@ export function updateSuitesStatus(tree, suites) { }); } -export function updateParentsChecked(tree, parentIds) { +export function updateParentsChecked(tree, parentIds, diff = tree) { const youngerSuites = [].concat(parentIds) .filter(parentId => parentId) .map((suiteId) => tree.suites.byId[suiteId]); const changeParentSuiteCb = (parentSuite) => { - changeSuiteState(tree, parentSuite.id, {checkStatus: shouldSuiteBeChecked(parentSuite, tree)}); + const checkStatus = shouldSuiteBeChecked(parentSuite, tree, diff); + changeSuiteState(tree, parentSuite.id, {checkStatus}, diff); }; youngerSuites.forEach(changeParentSuiteCb); @@ -57,49 +61,50 @@ export function getFailedRootSuiteIds(suites) { }); } -export function updateAllSuitesStatus(tree, filteredBrowsers) { +export function updateAllSuitesStatus(tree, filteredBrowsers, diff = tree) { const childSuitesIds = _(tree.browsers.allIds) .map((browserId) => tree.browsers.byId[browserId].parentId) .uniq() .value(); - return updateParentSuitesStatus(tree, childSuitesIds, filteredBrowsers); + return updateParentSuitesStatus(tree, childSuitesIds, filteredBrowsers, diff); } -export function calcSuitesShowness(tree, suiteIds) { +export function calcSuitesShowness({tree, suiteIds = [], diff = tree}) { const youngestSuites = _.isEmpty(suiteIds) ? getYoungestSuites(tree) - : [].concat(suiteIds).map((suiteId) => tree.suites.byId[suiteId]); + : suiteIds.map((suiteId) => tree.suites.byId[suiteId]); youngestSuites.forEach((suite) => { - const shouldBeShown = suite.browserIds - .some((browserId) => tree.browsers.stateById[browserId].shouldBeShown); - const checkStatus = shouldSuiteBeChecked(suite, tree); + const shouldBeShown = suite.browserIds.some((browserId) => { + return getUpdatedProperty(tree, diff, ['browsers', 'stateById', browserId, 'shouldBeShown']); + }); + const checkStatus = shouldSuiteBeChecked(suite, tree, diff); - changeSuiteState(tree, suite.id, {shouldBeShown, checkStatus}); + changeSuiteState(tree, suite.id, {shouldBeShown, checkStatus}, diff); }); const changeParentSuiteCb = (parentSuite) => { changeSuiteState(tree, parentSuite.id, { - shouldBeShown: shouldSuiteBeShown(parentSuite, tree), - checkStatus: shouldSuiteBeChecked(parentSuite, tree) - }); + shouldBeShown: shouldSuiteBeShown(parentSuite, tree, diff), + checkStatus: shouldSuiteBeChecked(parentSuite, tree, diff) + }, diff); }; calcParentSuitesState(youngestSuites, tree, changeParentSuiteCb); } -export function calcSuitesOpenness(tree, expand, suiteIds) { +export function calcSuitesOpenness({tree, expand, suiteIds = [], diff = tree}) { if (expand !== EXPAND_RETRIES) { if (_.isEmpty(suiteIds)) { suiteIds = tree.suites.allIds; } - [].concat(suiteIds).forEach((suiteId) => { + suiteIds.forEach((suiteId) => { const suite = tree.suites.byId[suiteId]; const shouldBeOpened = calcSuiteOpenness(suite, expand, tree); - changeSuiteState(tree, suiteId, {shouldBeOpened}); + changeSuiteState(tree, suiteId, {shouldBeOpened}, diff); }); return; @@ -115,7 +120,7 @@ export function calcSuitesOpenness(tree, expand, suiteIds) { }); const changeParentSuiteCb = (parentSuite) => { - changeSuiteState(tree, parentSuite.id, {shouldBeOpened: shouldSuiteBeOpened(parentSuite, tree)}); + changeSuiteState(tree, parentSuite.id, {shouldBeOpened: shouldSuiteBeOpened(parentSuite, tree)}, diff); }; calcParentSuitesState(youngestSuites, tree, changeParentSuiteCb); @@ -198,58 +203,69 @@ function calcSuiteOpenness(suite, expand, tree) { return shouldNodeBeOpened(expand, {errorsCb, retriesCb}); } -function shouldSuiteBeShown(suite, tree) { - return shouldSuiteBe(suite, tree, 'shouldBeShown'); +function shouldSuiteBeShown(suite, tree, diff) { + return shouldSuiteBe(suite, tree, 'shouldBeShown', diff); } -function shouldSuiteBeOpened(suite, tree) { - return shouldSuiteBe(suite, tree, 'shouldBeOpened'); +function shouldSuiteBeOpened(suite, tree, diff) { + return shouldSuiteBe(suite, tree, 'shouldBeOpened', diff); } -function shouldSuiteBeChecked(suite, tree) { - const shownChildSuiteCount = (tree.suites.byId[suite.id].suiteIds || []) - .reduce((count, childSuiteId) => count + tree.suites.stateById[childSuiteId].shouldBeShown, 0); - const shownChildBrowserCount = (tree.suites.byId[suite.id].browserIds || []) - .reduce((count, childBrowserId) => count + tree.browsers.stateById[childBrowserId].shouldBeShown, 0); +function shouldSuiteBeChecked(suite, tree, diff = tree) { + const shownChildSuiteCount = (tree.suites.byId[suite.id].suiteIds || []).reduce((count, childSuiteId) => { + return count + getUpdatedProperty(tree, diff, ['suites', 'stateById', childSuiteId, 'shouldBeShown']); + }, 0); + + const shownChildBrowserCount = (tree.suites.byId[suite.id].browserIds || []).reduce((count, childBrowserId) => { + return count + getUpdatedProperty(tree, diff, ['browsers', 'stateById', childBrowserId, 'shouldBeShown']); + }, 0); + const childCount = shownChildSuiteCount + shownChildBrowserCount; - const checkedChildCount = getShownCheckedChildCount(tree, suite.id); + const checkedChildCount = getShownCheckedChildCount(tree, suite.id, diff); return Number((checkedChildCount === childCount) || (checkedChildCount && INDETERMINATE)); } -function shouldSuiteBe(suite, tree, field) { - return (suite.suiteIds || []).some((suiteId) => tree.suites.stateById[suiteId][field]) - || (suite.browserIds || []).some((browserId) => tree.browsers.stateById[browserId][field]); +function shouldSuiteBe(suite, tree, field, diff) { + const someSuiteIs = (suite.suiteIds || []).some(suiteId => { + return getUpdatedProperty(tree, diff, ['suites', 'stateById', suiteId, field]); + }); + + return someSuiteIs || (suite.browserIds || []).some(browserId => { + return getUpdatedProperty(tree, diff, ['browsers', 'stateById', browserId, field]); + }); } -function updateParentSuitesStatus(tree, suitesIds = [], filteredBrowsers) { +function updateParentSuitesStatus(tree, suitesIds = [], filteredBrowsers, diff = tree) { if (!suitesIds || !suitesIds.length) { return; } - const suites = [].concat(suitesIds).map((id) => tree.suites.byId[id]); + const suites = suitesIds.map((id) => tree.suites.byId[id]); const parentsToUpdate = new Set(); suites.forEach((s) => { - const newStatus = getChildSuitesStatus(tree, s, filteredBrowsers); - if (newStatus === s.status) { + const newStatus = getChildSuitesStatus(tree, s, filteredBrowsers, diff); + if (newStatus === getUpdatedProperty(tree, diff, ['suites', 'byId', s.id, 'status'])) { return; } - s.status = newStatus; - if (s.parentId) { - parentsToUpdate.add(s.parentId); + _.set(diff, ['suites', 'byId', s.id, 'status'], newStatus); + + const parentId = getUpdatedProperty(tree, diff, ['suites', 'byId', s.id, 'parentId']); + if (parentId) { + parentsToUpdate.add(parentId); } }); - updateParentSuitesStatus(tree, Array.from(parentsToUpdate), filteredBrowsers); + updateParentSuitesStatus(tree, Array.from(parentsToUpdate), filteredBrowsers, diff); } -function getChildSuitesStatus(tree, suite, filteredBrowsers) { +function getChildSuitesStatus(tree, suite, filteredBrowsers, diff = tree) { let childStatuses = []; if (suite.suiteIds) { - childStatuses = suite.suiteIds.map((id) => tree.suites.byId[id].status); + childStatuses = suite.suiteIds.map((id) => getUpdatedProperty(tree, diff, ['suites', 'byId', id, 'status'])); } if (suite.browserIds) { @@ -263,7 +279,9 @@ function getChildSuitesStatus(tree, suite, filteredBrowsers) { return res; }); - const suiteBrowserStatuses = suiteBrowsers.map(({resultIds}) => tree.results.byId[_.last(resultIds)].status); + const suiteBrowserStatuses = suiteBrowsers.map(({resultIds}) => { + return getUpdatedProperty(tree, diff, ['results', 'byId', _.last(resultIds), 'status']); + }); childStatuses = childStatuses.concat(suiteBrowserStatuses); } diff --git a/lib/static/modules/utils.js b/lib/static/modules/utils.js index 569eb8178..2ef04aa54 100644 --- a/lib/static/modules/utils.js +++ b/lib/static/modules/utils.js @@ -1,6 +1,6 @@ 'use strict'; -const {get, isEmpty, find, isFunction, flatMap} = require('lodash'); +const {get, isEmpty, find, isFunction, flatMap, isPlainObject, isUndefined} = require('lodash'); const {isIdleStatus, isSuccessStatus, isUpdatedStatus, isFailStatus, isErroredStatus, isSkippedStatus} = require('../../common-utils'); const {getCommonErrors} = require('../../constants/errors'); const {ViewMode} = require('../../constants/view-modes'); @@ -81,7 +81,7 @@ function isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter) { : testName.includes(testNameFilter); } -function isBrowserMatchViewMode(browser, lastResult, viewMode) { +function isBrowserMatchViewMode(browser, lastResult, viewMode, diff = browser) { const {status} = lastResult; if (viewMode === ViewMode.ALL) { @@ -97,18 +97,18 @@ function isBrowserMatchViewMode(browser, lastResult, viewMode) { } if (viewMode === ViewMode.RETRIED) { - return browser.resultIds.length > 1; + return getUpdatedProperty(browser, diff, 'resultIds.length') > 1; } return status === viewMode; } -function shouldShowBrowser(browser, filteredBrowsers) { +function shouldShowBrowser(browser, filteredBrowsers, diff = browser) { if (isEmpty(filteredBrowsers)) { return true; } - const browserToFilterBy = find(filteredBrowsers, {id: browser.name}); + const browserToFilterBy = find(filteredBrowsers, {id: getUpdatedProperty(browser, diff, 'name')}); if (!browserToFilterBy) { return false; @@ -120,7 +120,7 @@ function shouldShowBrowser(browser, filteredBrowsers) { return true; } - return browserVersionsToFilterBy.includes(browser.version); + return browserVersionsToFilterBy.includes(getUpdatedProperty(browser, diff, 'version')); } function iterateSuites(node, {suiteCb, browserCb, browserIdsCb}) { @@ -159,6 +159,40 @@ function preloadImage(url) { new Image().src = url; } +function applyStateUpdate(state, diff) { + const result = {...state}; + + for (const key in diff) { + if (isPlainObject(diff[key]) && isPlainObject(state[key])) { + result[key] = applyStateUpdate(state[key], diff[key]); + } else if (diff[key] !== undefined) { + result[key] = diff[key]; + } else { + delete result[key]; + } + } + + return result; +} + +function ensureDiffProperty(diff, path) { + let state = diff; + + for (let i = 0; i < path.length; i++) { + const property = path[i]; + + state[property] = state[property] || {}; + + state = state[property]; + } +} + +function getUpdatedProperty(state, diff, path) { + const diffValue = get(diff, path); + + return isUndefined(diffValue) ? get(state, path) : diffValue; +} + module.exports = { isNoRefImageError, isAssertViewError, @@ -177,5 +211,8 @@ module.exports = { shouldShowBrowser, iterateSuites, parseKeyToGroupTestsBy, - preloadImage + preloadImage, + applyStateUpdate, + ensureDiffProperty, + getUpdatedProperty }; diff --git a/lib/static/modules/utils/index.js b/lib/static/modules/utils/index.js new file mode 100644 index 000000000..19f395351 --- /dev/null +++ b/lib/static/modules/utils/index.js @@ -0,0 +1,182 @@ +'use strict'; + +const {get, isEmpty, find, isFunction, flatMap} = require('lodash'); +const {isIdleStatus, isSuccessStatus, isUpdatedStatus, isFailStatus, isErroredStatus, isSkippedStatus} = require('../../common-utils'); +const {getCommonErrors} = require('../../constants/errors'); +const {ViewMode} = require('../../constants/view-modes'); +const {SECTIONS, RESULT_KEYS, KEY_DELIMITER} = require('../../constants/group-tests'); +const {getUpdatedProperty} = require('./state'); + +const AVAILABLE_GROUP_SECTIONS = Object.values(SECTIONS); +const {NO_REF_IMAGE_ERROR, ASSERT_VIEW_ERROR} = getCommonErrors(); + +function hasFailedImages(result) { + const {imagesInfo = []} = result; + + return imagesInfo.some(({error, status}) => !isAssertViewError(error) && (isErroredStatus(status) || isFailStatus(status))); +} + +function isNoRefImageError(error) { + const stack = get(error, 'stack', ''); + return stack.startsWith(NO_REF_IMAGE_ERROR); +} + +function isAssertViewError(error) { + const stack = get(error, 'stack', ''); + return stack.startsWith(ASSERT_VIEW_ERROR); +} + +function hasNoRefImageErrors({imagesInfo = []}) { + return Boolean(imagesInfo.filter(({error}) => isNoRefImageError(error)).length); +} + +function hasResultFails(testResult) { + return hasFailedImages(testResult) || isErroredStatus(testResult.status) || isFailStatus(testResult.status); +} + +function isSuiteIdle(suite) { + return isIdleStatus(suite.status); +} + +function isSuiteSuccessful(suite) { + return isSuccessStatus(suite.status); +} + +function isNodeFailed(node) { + return isFailStatus(node.status) || isErroredStatus(node.status); +} + +function isNodeSuccessful(node) { + return isSuccessStatus(node.status) || isUpdatedStatus(node.status); +} + +function isAcceptable({status, error}) { + return isErroredStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status); +} + +function isScreenRevertable({gui, image, isLastResult}) { + return gui && image.stateName && isLastResult && isUpdatedStatus(image.status); +} + +function dateToLocaleString(date) { + if (!date) { + return ''; + } + const lang = isEmpty(navigator.languages) ? navigator.language : navigator.languages[0]; + return new Date(date).toLocaleString(lang); +} + +function getHttpErrorMessage(error) { + const {message, response} = error; + + return response ? `(${response.status}) ${response.data}` : message; +} + +function isTestNameMatchFilters(testName, testNameFilter, strictMatchFilter) { + if (!testNameFilter) { + return true; + } + + return strictMatchFilter + ? testName === testNameFilter + : testName.includes(testNameFilter); +} + +function isBrowserMatchViewMode(browser, lastResult, viewMode, diff = browser) { + const {status} = lastResult; + + if (viewMode === ViewMode.ALL) { + return true; + } + + if (viewMode === ViewMode.PASSED && isSuccessStatus(status)) { + return true; + } + + if (viewMode === ViewMode.FAILED && (isFailStatus(status) || isErroredStatus(status))) { + return true; + } + + if (viewMode === ViewMode.RETRIED) { + return getUpdatedProperty(browser, diff, 'resultIds.length') > 1; + } + + return status === viewMode; +} + +function shouldShowBrowser(browser, filteredBrowsers, diff = browser) { + if (isEmpty(filteredBrowsers)) { + return true; + } + + const browserToFilterBy = find(filteredBrowsers, {id: getUpdatedProperty(browser, diff, 'name')}); + + if (!browserToFilterBy) { + return false; + } + + const browserVersionsToFilterBy = [].concat(browserToFilterBy.versions).filter(Boolean); + + if (isEmpty(browserVersionsToFilterBy)) { + return true; + } + + return browserVersionsToFilterBy.includes(getUpdatedProperty(browser, diff, 'version')); +} + +function iterateSuites(node, {suiteCb, browserCb, browserIdsCb}) { + let resultFromBrowsers = []; + let resultFromSuites = []; + + if (node.browserIds && [browserCb, browserIdsCb].some(isFunction)) { + resultFromBrowsers = browserIdsCb + ? browserIdsCb(node.browserIds, node) + : flatMap(node.browserIds, (browserId) => browserCb(browserId, node)); + } + + if (node.suiteIds && isFunction(suiteCb)) { + resultFromSuites = flatMap(node.suiteIds, (suiteId) => suiteCb(suiteId, node)); + } + + return [...resultFromBrowsers, ...resultFromSuites]; +} + +function parseKeyToGroupTestsBy(key) { + let [groupSection, ...groupKey] = key.split(KEY_DELIMITER); + groupKey = groupKey.join(KEY_DELIMITER); + + if (!AVAILABLE_GROUP_SECTIONS.includes(groupSection)) { + throw new Error(`Group section must be one of ${AVAILABLE_GROUP_SECTIONS.join(', ')}, but got ${groupSection}`); + } + + if (groupSection === SECTIONS.RESULT && !RESULT_KEYS.includes(groupKey)) { + throw new Error(`Group key must be one of ${RESULT_KEYS.join(', ')}, but got ${groupKey}`); + } + + return [groupSection, groupKey]; +} + +function preloadImage(url) { + new Image().src = url; +} + +module.exports = { + isNoRefImageError, + isAssertViewError, + hasNoRefImageErrors, + hasResultFails, + isSuiteIdle, + isSuiteSuccessful, + isNodeFailed, + isNodeSuccessful, + isAcceptable, + isScreenRevertable, + dateToLocaleString, + getHttpErrorMessage, + isTestNameMatchFilters, + isBrowserMatchViewMode, + shouldShowBrowser, + iterateSuites, + parseKeyToGroupTestsBy, + preloadImage +}; diff --git a/lib/static/modules/utils/state.js b/lib/static/modules/utils/state.js new file mode 100644 index 000000000..07610753a --- /dev/null +++ b/lib/static/modules/utils/state.js @@ -0,0 +1,60 @@ +const {get, isPlainObject, isUndefined} = require('lodash'); + +/** + * Create new state from old state and diff object + * @param {Object} state + * @param {Object} diff + * @returns {Object} new state, created by overlaying diff to state + */ +function applyStateUpdate(state, diff) { + const result = {...state}; + + for (const key in diff) { + if (isPlainObject(diff[key]) && isPlainObject(state[key])) { + result[key] = applyStateUpdate(state[key], diff[key]); + } else if (diff[key] !== undefined) { + result[key] = diff[key]; + } else { + delete result[key]; + } + } + + return result; +} + +/** + * Ensure diff has an object by given path + * Usually it is being used to pass nested diff property to a helper function + * @param {Object} diff + * @param {Array} path + */ +function ensureDiffProperty(diff, path) { + let state = diff; + + for (let i = 0; i < path.length; i++) { + const property = path[i]; + + state[property] = state[property] || {}; + + state = state[property]; + } +} + +/** + * + * @param {Object} state + * @param {Object} diff + * @param {string|Array} path - in _.get style + * @returns result of _.get(diff, path) if exists, _.get(state, path) else + */ +function getUpdatedProperty(state, diff, path) { + const diffValue = get(diff, path); + + return isUndefined(diffValue) ? get(state, path) : diffValue; +} + +module.exports = { + applyStateUpdate, + ensureDiffProperty, + getUpdatedProperty +}; diff --git a/test/unit/lib/static/modules/reducers/grouped-tests/by/meta.js b/test/unit/lib/static/modules/reducers/grouped-tests/by/meta.js index 0a02292aa..ad3e957ba 100644 --- a/test/unit/lib/static/modules/reducers/grouped-tests/by/meta.js +++ b/test/unit/lib/static/modules/reducers/grouped-tests/by/meta.js @@ -79,8 +79,8 @@ describe('lib/static/modules/reducers/grouped-tests/by/meta', () => { module.groupMeta({group, groupKey: 'aaa'}); assert.calledTwice(addGroupItem); - assert.calledWith(addGroupItem.firstCall, {group: sinon.match.object, result: results[0], value: 1}); - assert.calledWith(addGroupItem.secondCall, {group: sinon.match.object, result: results[1], value: 3}); + assert.calledWith(addGroupItem.firstCall, {group: sinon.match.object, diff: {}, result: results[0], value: 1}); + assert.calledWith(addGroupItem.secondCall, {group: sinon.match.object, diff: {}, result: results[1], value: 3}); }); it('should sort group values by passed meta key', () => { diff --git a/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js b/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js index c2fa7c62d..6a526307a 100644 --- a/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js +++ b/test/unit/lib/static/modules/reducers/grouped-tests/by/result.js @@ -52,11 +52,11 @@ describe('lib/static/modules/reducers/grouped-tests/by/result', () => { assert.calledTwice(addGroupItem); assert.calledWith( addGroupItem.firstCall, - {group: sinon.match.object, result: results[0], value: 'err-1', patterns: []} + {group: sinon.match.object, diff: {}, result: results[0], value: 'err-1', patterns: []} ); assert.calledWith( addGroupItem.secondCall, - {group: sinon.match.object, result: results[1], value: 'err-2', patterns: []} + {group: sinon.match.object, diff: {}, result: results[1], value: 'err-2', patterns: []} ); }); @@ -74,11 +74,11 @@ describe('lib/static/modules/reducers/grouped-tests/by/result', () => { assert.calledTwice(addGroupItem); assert.calledWith( addGroupItem.firstCall, - {group: sinon.match.object, result: results[0], value: 'img-err', patterns: []} + {group: sinon.match.object, diff: {}, result: results[0], value: 'img-err', patterns: []} ); assert.calledWith( addGroupItem.secondCall, - {group: sinon.match.object, result: results[0], value: 'err-1', patterns: []} + {group: sinon.match.object, diff: {}, result: results[0], value: 'err-1', patterns: []} ); }); @@ -96,7 +96,7 @@ describe('lib/static/modules/reducers/grouped-tests/by/result', () => { assert.calledOnceWith( addGroupItem, - {group: sinon.match.object, result: results[0], value: 'img-err', patterns: []} + {group: sinon.match.object, diff: {}, result: results[0], value: 'img-err', patterns: []} ); }); @@ -111,7 +111,7 @@ describe('lib/static/modules/reducers/grouped-tests/by/result', () => { assert.calledOnceWith( addGroupItem, - {group: sinon.match.object, result: results[0], value: 'image comparison failed', patterns: []} + {group: sinon.match.object, diff: {}, result: results[0], value: 'image comparison failed', patterns: []} ); }); diff --git a/test/unit/lib/static/modules/reducers/grouped-tests/index.js b/test/unit/lib/static/modules/reducers/grouped-tests/index.js index 3348b6d1e..76c5de1b4 100644 --- a/test/unit/lib/static/modules/reducers/grouped-tests/index.js +++ b/test/unit/lib/static/modules/reducers/grouped-tests/index.js @@ -55,7 +55,7 @@ describe('lib/static/modules/reducers/grouped-tests', () => { reducer(state, {type: actionName}); - assert.calledOnceWith(groupMeta, {tree: state.tree, group: state.groupedTests.meta, ...viewArgs}); + assert.calledOnceWith(groupMeta, {tree: state.tree, group: state.groupedTests.meta, diff: {}, ...viewArgs}); assert.notCalled(groupResult); }); @@ -74,6 +74,7 @@ describe('lib/static/modules/reducers/grouped-tests', () => { assert.calledOnceWith(groupResult, { tree: state.tree, group: state.groupedTests[SECTIONS.RESULT], + diff: {}, groupKey: ERROR_KEY, errorPatterns: state.config.errorPatterns, ...viewArgs @@ -97,6 +98,7 @@ describe('lib/static/modules/reducers/grouped-tests', () => { tree: state.tree, group: state.groupedTests[SECTIONS.META], groupKey, + diff: {}, ...viewArgs }); assert.notCalled(groupResult);