From 3a07210cd71f15653330226479c1264f3f30a434 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 27 Dec 2024 03:16:44 +0300 Subject: [PATCH 1/2] refactor: re-write actions to typescript and implement new analytics API --- lib/common-utils.ts | 4 +- .../controls/accept-opened-button.jsx | 10 +- .../controls/custom-gui-controls.jsx | 2 +- .../controls/find-same-diffs-button.jsx | 2 +- .../components/controls/gui-controls.jsx | 2 +- .../components/controls/run-button/index.jsx | 6 +- lib/static/components/gui.jsx | 4 +- .../modals/screenshot-accepter/index.jsx | 12 +- lib/static/components/report.jsx | 2 +- lib/static/components/section/body/index.jsx | 2 +- .../components/section/title/simple.jsx | 2 +- lib/static/components/state/index.jsx | 15 +- lib/static/gui.jsx | 5 +- lib/static/index.jsx | 5 +- lib/static/modules/action-names.ts | 20 +- lib/static/modules/actions/custom-gui.ts | 25 ++ lib/static/modules/actions/filter-tests.ts | 36 ++ lib/static/modules/actions/find-same-diffs.ts | 45 +++ lib/static/modules/actions/group-tests.ts | 9 +- lib/static/modules/actions/index.js | 273 ------------- lib/static/modules/actions/index.ts | 18 + lib/static/modules/actions/lifecycle.ts | 12 +- lib/static/modules/actions/loading.ts | 11 + lib/static/modules/actions/modals.ts | 18 + lib/static/modules/actions/notifications.ts | 27 ++ lib/static/modules/actions/processing.ts | 12 + lib/static/modules/actions/run-tests.ts | 116 ++++++ lib/static/modules/actions/screenshots.ts | 74 ++++ lib/static/modules/actions/settings.ts | 17 + lib/static/modules/actions/static-accepter.ts | 14 +- .../modules/actions/suites-tree-state.ts | 102 +++++ lib/static/modules/actions/suites.ts | 13 - lib/static/modules/actions/types.ts | 34 +- lib/static/modules/middlewares/metrika.js | 154 ------- lib/static/modules/middlewares/metrika.ts | 97 +++++ .../modules/reducers/grouped-tests/index.js | 5 +- lib/static/modules/reducers/tree/index.js | 17 +- lib/static/modules/reducers/view.js | 29 +- lib/static/modules/store.js | 17 +- lib/static/modules/utils/performance.ts | 9 + lib/static/modules/web-vitals.js | 12 - lib/static/modules/web-vitals.ts | 9 + lib/static/modules/yandex-metrika.js | 52 --- lib/static/modules/yandex-metrika.ts | 67 ++++ lib/static/new-ui/app/App.tsx | 30 +- lib/static/new-ui/app/gui.tsx | 11 +- .../new-ui/components/AttemptPicker/index.tsx | 4 +- .../components/CustomScripts/index.tsx} | 22 +- .../ScreenshotsTreeViewItem/index.tsx | 20 +- .../components/TestStatusFilter/index.tsx | 2 +- .../components/TreeActionsToolbar/index.tsx | 17 +- .../components/VisualChecksPage/index.tsx | 17 +- lib/static/new-ui/hooks/useAnalytics.ts | 15 + lib/static/new-ui/providers/analytics.tsx | 19 + lib/static/new-ui/utils/analytics.ts | 21 + lib/tests-tree-builder/base.ts | 6 +- lib/types.ts | 2 +- .../controls/accept-opened-button.jsx | 6 +- .../controls/custom-gui-controls.jsx | 4 +- .../controls/find-same-diffs-button.jsx | 4 +- .../components/controls/gui-controls.jsx | 4 +- .../static/components/controls/run-button.jsx | 16 +- .../lib/static/components/custom-scripts.tsx | 34 +- .../modals/screenshot-accepter/index.jsx | 2 +- .../static/components/section/body/index.jsx | 6 +- .../lib/static/components/state/index.jsx | 10 +- .../lib/static/modules/actions/custom-gui.ts | 92 +++++ test/unit/lib/static/modules/actions/index.js | 199 +-------- .../lib/static/modules/actions/lifecycle.ts | 30 +- .../lib/static/modules/actions/run-tests.ts | 97 +++++ .../lib/static/modules/middlewares/metrika.js | 377 ++++-------------- .../modules/reducers/grouped-tests/index.js | 4 +- .../lib/static/modules/reducers/tree/index.js | 28 +- test/unit/lib/static/modules/web-vitals.js | 6 - .../unit/lib/static/modules/yandex-metrika.js | 26 +- 75 files changed, 1293 insertions(+), 1255 deletions(-) create mode 100644 lib/static/modules/actions/custom-gui.ts create mode 100644 lib/static/modules/actions/filter-tests.ts create mode 100644 lib/static/modules/actions/find-same-diffs.ts delete mode 100644 lib/static/modules/actions/index.js create mode 100644 lib/static/modules/actions/index.ts create mode 100644 lib/static/modules/actions/loading.ts create mode 100644 lib/static/modules/actions/modals.ts create mode 100644 lib/static/modules/actions/notifications.ts create mode 100644 lib/static/modules/actions/processing.ts create mode 100644 lib/static/modules/actions/run-tests.ts create mode 100644 lib/static/modules/actions/screenshots.ts create mode 100644 lib/static/modules/actions/settings.ts create mode 100644 lib/static/modules/actions/suites-tree-state.ts delete mode 100644 lib/static/modules/actions/suites.ts delete mode 100644 lib/static/modules/middlewares/metrika.js create mode 100644 lib/static/modules/middlewares/metrika.ts create mode 100644 lib/static/modules/utils/performance.ts delete mode 100644 lib/static/modules/web-vitals.js create mode 100644 lib/static/modules/web-vitals.ts delete mode 100644 lib/static/modules/yandex-metrika.js create mode 100644 lib/static/modules/yandex-metrika.ts rename lib/static/{components/custom-scripts.jsx => new-ui/components/CustomScripts/index.tsx} (50%) create mode 100644 lib/static/new-ui/hooks/useAnalytics.ts create mode 100644 lib/static/new-ui/providers/analytics.tsx create mode 100644 lib/static/new-ui/utils/analytics.ts create mode 100644 test/unit/lib/static/modules/actions/custom-gui.ts create mode 100644 test/unit/lib/static/modules/actions/run-tests.ts diff --git a/lib/common-utils.ts b/lib/common-utils.ts index 9da559bd3..d6b1e85b7 100644 --- a/lib/common-utils.ts +++ b/lib/common-utils.ts @@ -16,7 +16,7 @@ import { COMMITED } from './constants'; -import {CHECKED, INDETERMINATE, UNCHECKED} from './constants/checked-statuses'; +import {CHECKED, CheckStatus, INDETERMINATE, UNCHECKED} from './constants/checked-statuses'; import { ImageBase64, ImageBuffer, @@ -247,7 +247,7 @@ export const normalizeUrls = (urls: string[] = [], baseUrl: string): string[] => export const isCheckboxChecked = (status: number): boolean => Number(status) === CHECKED; export const isCheckboxIndeterminate = (status: number): boolean => Number(status) === INDETERMINATE; export const isCheckboxUnchecked = (status: number): boolean => Number(status) === UNCHECKED; -export const getToggledCheckboxState = (status: number): number => isCheckboxChecked(status) ? UNCHECKED : CHECKED; +export const getToggledCheckboxState = (status: number): CheckStatus => isCheckboxChecked(status) ? UNCHECKED : CHECKED; export function getDetailsFileName(testId: string, browserId: string, attempt: number): string { return `${testId}-${browserId}_${Number(attempt) + 1}_${Date.now()}.json`; diff --git a/lib/static/components/controls/accept-opened-button.jsx b/lib/static/components/controls/accept-opened-button.jsx index 376912a03..e5c587710 100644 --- a/lib/static/components/controls/accept-opened-button.jsx +++ b/lib/static/components/controls/accept-opened-button.jsx @@ -5,8 +5,11 @@ import PropTypes from 'prop-types'; import * as actions from '../../modules/actions'; import ControlButton from './control-button'; import {getAcceptableOpenedImageIds} from '../../modules/selectors/tree'; +import {AnalyticsContext} from '@/static/new-ui/providers/analytics'; class AcceptOpenedButton extends Component { + static contextType = AnalyticsContext; + static propTypes = { isSuiteContol: PropTypes.bool, // from store @@ -16,11 +19,14 @@ class AcceptOpenedButton extends Component { actions: PropTypes.object.isRequired }; - _acceptOpened = () => { + _acceptOpened = async () => { + const analytics = this.context; + await analytics?.trackOpenedScreenshotsAccept({acceptedImagesCount: this.props.acceptableOpenedImageIds.length}); + if (this.props.isStaticImageAccepterEnabled) { this.props.actions.staticAccepterStageScreenshot(this.props.acceptableOpenedImageIds); } else { - this.props.actions.acceptOpened(this.props.acceptableOpenedImageIds); + this.props.actions.thunkAcceptImages({imageIds: this.props.acceptableOpenedImageIds}); } }; diff --git a/lib/static/components/controls/custom-gui-controls.jsx b/lib/static/components/controls/custom-gui-controls.jsx index f9eeb0990..c665dc0ce 100644 --- a/lib/static/components/controls/custom-gui-controls.jsx +++ b/lib/static/components/controls/custom-gui-controls.jsx @@ -29,7 +29,7 @@ class CustomGuiControls extends PureComponent { const onClickHandler = (event, {value}) => { const controlIndex = controls.findIndex((control) => control.value === value); - actions.runCustomGuiAction({sectionName, groupIndex, controlIndex}); + actions.thunkRunCustomGuiAction({sectionName, groupIndex, controlIndex}); }; return map(controls, ({label, value, active}, i) => diff --git a/lib/static/components/controls/find-same-diffs-button.jsx b/lib/static/components/controls/find-same-diffs-button.jsx index f2daf645f..33fe57086 100644 --- a/lib/static/components/controls/find-same-diffs-button.jsx +++ b/lib/static/components/controls/find-same-diffs-button.jsx @@ -21,7 +21,7 @@ class FindSameDiffsButton extends Component { _findSameDiffs = () => { const {actions, imageId, failedOpenedImageIds, browserName} = this.props; - actions.findSameDiffs(imageId, failedOpenedImageIds, browserName); + actions.thunkFindSameDiffs(imageId, failedOpenedImageIds, browserName); }; render() { diff --git a/lib/static/components/controls/gui-controls.jsx b/lib/static/components/controls/gui-controls.jsx index 6cb3b395a..014e4e39b 100644 --- a/lib/static/components/controls/gui-controls.jsx +++ b/lib/static/components/controls/gui-controls.jsx @@ -30,7 +30,7 @@ class GuiControls extends Component { diff --git a/lib/static/components/controls/run-button/index.jsx b/lib/static/components/controls/run-button/index.jsx index 800e0e94d..5de0edb6a 100644 --- a/lib/static/components/controls/run-button/index.jsx +++ b/lib/static/components/controls/run-button/index.jsx @@ -29,9 +29,9 @@ const RunButton = ({actions, autoRun, isDisabled, isRunning, failedTests, checke const selectFailedTests = () => !shouldDisableFailed && setMode(RunMode.FAILED); const selectCheckedTests = () => !shouldDisableChecked && setMode(RunMode.CHECKED); - const runAllTests = () => actions.runAllTests(); - const runFailedTests = () => actions.runFailedTests(failedTests); - const runCheckedTests = () => actions.retrySuite(checkedTests); + const runAllTests = () => actions.thunkRunAllTests(); + const runFailedTests = () => actions.thunkRunFailedTests({tests: failedTests}); + const runCheckedTests = () => actions.thunkRunSuite({tests: checkedTests}); const handleRunClick = () => { const action = { diff --git a/lib/static/components/gui.jsx b/lib/static/components/gui.jsx index be2857401..6c4340673 100644 --- a/lib/static/components/gui.jsx +++ b/lib/static/components/gui.jsx @@ -10,7 +10,7 @@ import StickyHeader from './sticky-header/gui'; import Loading from './loading'; import ModalContainer from '../containers/modal'; import MainTree from './main-tree'; -import CustomScripts from './custom-scripts'; +import {CustomScripts} from '../new-ui/components/CustomScripts'; import {ClientEvents} from '../../gui/constants/client-events'; import FaviconChanger from './favicon-changer'; import ExtensionPoint from './extension-point'; @@ -60,7 +60,7 @@ class Gui extends Component { }); eventSource.addEventListener(ClientEvents.END, () => { - actions.testsEnd(); + actions.thunkTestsEnd(); }); } diff --git a/lib/static/components/modals/screenshot-accepter/index.jsx b/lib/static/components/modals/screenshot-accepter/index.jsx index f4f55a771..e46a9a3b6 100644 --- a/lib/static/components/modals/screenshot-accepter/index.jsx +++ b/lib/static/components/modals/screenshot-accepter/index.jsx @@ -13,10 +13,13 @@ import {staticImageAccepterPropType} from '../../../modules/static-image-accepte import {preloadImage} from '../../../modules/utils'; import './style.css'; +import {AnalyticsContext} from '@/static/new-ui/providers/analytics'; const PRELOAD_IMAGE_COUNT = 3; class ScreenshotAccepter extends Component { + static contextType = AnalyticsContext; + static propTypes = { view: PropTypes.shape({ diffMode: PropTypes.string.isRequired @@ -58,6 +61,8 @@ class ScreenshotAccepter extends Component { for (let i = 1; i <= PRELOAD_IMAGE_COUNT; i++) { this._preloadAdjacentImages(activeImageIndex, stateNameImageIds, i); } + + this.analytics = this.context; } componentDidUpdate() { @@ -138,7 +143,7 @@ class ScreenshotAccepter extends Component { if (this.props.staticImageAccepter.enabled) { this.props.actions.staticAccepterUndoDelayScreenshot(); } else { - await this.props.actions.undoAcceptImage(imageId, {skipTreeUpdate: true}); + await this.props.actions.thunkRevertImages({imageIds: [imageId], shouldCommitUpdatesToTree: false}); this.delayedTestResults.pop(); } @@ -164,7 +169,7 @@ class ScreenshotAccepter extends Component { this.props.actions.staticAccepterStageScreenshot(imageIdsToStage); } else { - this.props.actions.applyDelayedTestResults(this.delayedTestResults); + this.props.actions.commitAcceptedImagesToTree(this.delayedTestResults); } this.props.onClose(); @@ -184,7 +189,8 @@ class ScreenshotAccepter extends Component { } async _acceptScreenshot(imageId, stateName) { - const updatedData = await this.props.actions.screenshotAccepterAccept(imageId); + const updatedData = await this.props.actions.thunkAcceptImages({imageIds: [imageId], shouldCommitUpdatesToTree: false}); + this.analytics?.trackScreenshotsAccept(); if (updatedData === null) { return null; diff --git a/lib/static/components/report.jsx b/lib/static/components/report.jsx index a1b818c66..4b9104451 100644 --- a/lib/static/components/report.jsx +++ b/lib/static/components/report.jsx @@ -10,7 +10,7 @@ import Loading from './loading'; import StickyHeader from './sticky-header/report'; import ModalContainer from '../containers/modal'; import MainTree from './main-tree'; -import CustomScripts from './custom-scripts'; +import {CustomScripts} from '../new-ui/components/CustomScripts'; import FaviconChanger from './favicon-changer'; import ExtensionPoint from './extension-point'; import BottomProgressBar from './bottom-progress-bar'; diff --git a/lib/static/components/section/body/index.jsx b/lib/static/components/section/body/index.jsx index ac29b0cf9..f6ac2f5be 100644 --- a/lib/static/components/section/body/index.jsx +++ b/lib/static/components/section/body/index.jsx @@ -27,7 +27,7 @@ function Body(props) { const onTestRetry = () => { const {testName, browserName} = props; - props.actions.retryTest({testName, browserName}); + props.actions.thunkRunTest({test: {testName, browserName}}); }; const addRetrySwitcher = () => { diff --git a/lib/static/components/section/title/simple.jsx b/lib/static/components/section/title/simple.jsx index bc71320d9..882149393 100644 --- a/lib/static/components/section/title/simple.jsx +++ b/lib/static/components/section/title/simple.jsx @@ -22,7 +22,7 @@ const SectionTitle = ({name, suiteId, handler, gui, checkStatus, suiteTests, act const onSuiteRetry = (e) => { e.stopPropagation(); - actions.retrySuite(suiteTests); + actions.thunkRunSuite({tests: suiteTests}); }; const onToggleCheckbox = (e) => { diff --git a/lib/static/components/state/index.jsx b/lib/static/components/state/index.jsx index 587c38571..47ad703af 100644 --- a/lib/static/components/state/index.jsx +++ b/lib/static/components/state/index.jsx @@ -17,8 +17,11 @@ import {isSuccessStatus, isFailStatus, isErrorStatus, isUpdatedStatus, isIdleSta import {Disclosure} from '@gravity-ui/uikit'; import {ChevronsExpandUpRight, Check, ArrowUturnCcwDown} from '@gravity-ui/icons'; import {getDisplayedDiffPercentValue} from '@/static/new-ui/components/DiffViewer/utils'; +import {AnalyticsContext} from '@/static/new-ui/providers/analytics'; class State extends Component { + static contextType = AnalyticsContext; + static propTypes = { result: PropTypes.shape({ status: PropTypes.string.isRequired, @@ -49,6 +52,12 @@ class State extends Component { actions: PropTypes.object.isRequired }; + constructor(props) { + super(props); + + this.analytics = this.context; + } + toggleModal = () => { const {actions, image} = this.props; @@ -71,10 +80,12 @@ class State extends Component { }; onTestAccept = () => { + this.analytics?.trackScreenshotsAccept(); + if (this.props.isStaticImageAccepterEnabled) { this.props.actions.staticAccepterStageScreenshot([this.props.imageId]); } else { - this.props.actions.acceptTest(this.props.imageId); + this.props.actions.thunkAcceptImages({imageIds: [this.props.imageId]}); } }; @@ -82,7 +93,7 @@ class State extends Component { if (this.props.isStaticImageAccepterEnabled) { this.props.actions.staticAccepterUnstageScreenshot([this.props.imageId]); } else { - this.props.actions.undoAcceptImage(this.props.imageId); + this.props.actions.thunkRevertImages({imageIds: [this.props.imageId]}); } }; diff --git a/lib/static/gui.jsx b/lib/static/gui.jsx index 2637afb6b..14be7ee85 100644 --- a/lib/static/gui.jsx +++ b/lib/static/gui.jsx @@ -7,6 +7,7 @@ import {ThemeProvider} from '@gravity-ui/uikit'; import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; +import {AnalyticsProvider} from '@/static/new-ui/providers/analytics'; const rootEl = document.getElementById('app'); const root = createRoot(rootEl); @@ -14,7 +15,9 @@ const root = createRoot(rootEl); root.render( - + + + ); diff --git a/lib/static/index.jsx b/lib/static/index.jsx index c4fda6fd6..80b43f5b4 100644 --- a/lib/static/index.jsx +++ b/lib/static/index.jsx @@ -7,6 +7,7 @@ import {ThemeProvider} from '@gravity-ui/uikit'; import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; +import {AnalyticsProvider} from '@/static/new-ui/providers/analytics'; const rootEl = document.getElementById('app'); const root = createRoot(rootEl); @@ -14,7 +15,9 @@ const root = createRoot(rootEl); root.render( - + + + ); diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 098b5e10d..c438968dd 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -12,11 +12,8 @@ export default { TEST_BEGIN: 'TEST_BEGIN', TEST_RESULT: 'TEST_RESULT', TESTS_END: 'TESTS_END', - ACCEPT_SCREENSHOT: 'ACCEPT_SCREENSHOT', - ACCEPT_OPENED_SCREENSHOTS: 'ACCEPT_OPENED_SCREENSHOTS', - SCREENSHOT_ACCEPTER_ACCEPT: 'SCREENSHOT_ACCEPTER/ACCEPT', - APPLY_DELAYED_TEST_RESULTS: 'APPLY_DELAYED_TEST_RESULTS', - UNDO_ACCEPT_IMAGES: 'UNDO_ACCEPT_IMAGES', + COMMIT_ACCEPTED_IMAGES_TO_TREE: 'COMMIT_ACCEPTED_IMAGES_TO_TREE', + COMMIT_REVERTED_IMAGES_TO_TREE: 'COMMIT_REVERTED_IMAGES_TO_TREE', STATIC_ACCEPTER_DELAY_SCREENSHOT: 'STATIC_ACCEPTER_DELAY_SCREENSHOT', STATIC_ACCEPTER_UNDO_DELAY_SCREENSHOT: 'STATIC_ACCEPTER_UNDO_DELAY_SCREENSHOT', STATIC_ACCEPTER_STAGE_SCREENSHOT: 'STATIC_ACCEPTER_STAGE_SCREENSHOT', @@ -38,24 +35,13 @@ export default { VIEW_UPDATE_BASE_HOST: 'VIEW_UPDATE_BASE_HOST', VIEW_UPDATE_FILTER_BY_NAME: 'VIEW_UPDATE_FILTER_BY_NAME', VIEW_SET_STRICT_MATCH_FILTER: 'VIEW_SET_STRICT_MATCH_FILTER', - VIEW_THREE_UP_DIFF: 'VIEW_THREE_UP_DIFF', - VIEW_THREE_UP_SCALED_DIFF: 'VIEW_THREE_UP_SCALED_DIFF', - VIEW_THREE_UP_SCALED_TO_FIT_DIFF: 'VIEW_THREE_UP_SCALED_TO_FIT_DIFF', - VIEW_ONLY_DIFF: 'VIEW_ONLY_DIFF', - VIEW_SWITCH_DIFF: 'VIEW_SWITCH_DIFF', - VIEW_SWIPE_DIFF: 'VIEW_SWIPE_DIFF', - VIEW_ONION_SKIN_DIFF: 'VIEW_ONION_SKIN_DIFF', + SET_DIFF_MODE: 'SET_DIFF_MODE', PROCESS_BEGIN: 'PROCESS_BEGIN', PROCESS_END: 'PROCESS_END', RUN_CUSTOM_GUI_ACTION: 'RUN_CUSTOM_GUI_ACTION', BROWSERS_SELECTED: 'BROWSERS_SELECTED', - COPY_SUITE_NAME: 'COPY_SUITE_NAME', - VIEW_IN_BROWSER: 'VIEW_IN_BROWSER', - COPY_TEST_LINK: 'COPY_TEST_LINK', TOGGLE_SUITE_SECTION: 'TOGGLE_SUITE_SECTION', TOGGLE_BROWSER_SECTION: 'TOGGLE_BROWSER_SECTION', - TOGGLE_META_INFO: 'TOGGLE_META_INFO', - TOGGLE_PAGE_SCREENSHOT: 'TOGGLE_PAGE_SCREENSHOT', TOGGLE_TESTS_GROUP: 'TOGGLE_TESTS_GROUP', TOGGLE_SUITE_CHECKBOX: 'TOGGLE_SUITE_CHECKBOX', TOGGLE_GROUP_CHECKBOX: 'TOGGLE_GROUP_CHECKBOX', diff --git a/lib/static/modules/actions/custom-gui.ts b/lib/static/modules/actions/custom-gui.ts new file mode 100644 index 000000000..c740b6dab --- /dev/null +++ b/lib/static/modules/actions/custom-gui.ts @@ -0,0 +1,25 @@ +import axios from 'axios'; +import actionNames from '@/static/modules/action-names'; +import {type Action, AppThunk} from '@/static/modules/actions/types'; +import {createNotificationError} from '@/static/modules/actions/notifications'; +import {CustomGuiActionPayload} from '@/adapters/tool/types'; + +export type RunCustomGuiAction = Action; +export const runCustomGui = (payload: RunCustomGuiAction['payload']): RunCustomGuiAction => ({type: actionNames.RUN_CUSTOM_GUI_ACTION, payload}); + +export const thunkRunCustomGuiAction = (payload: CustomGuiActionPayload): AppThunk => { + return async (dispatch) => { + try { + const {sectionName, groupIndex, controlIndex} = payload; + + await axios.post('/run-custom-gui-action', {sectionName, groupIndex, controlIndex}); + + dispatch(runCustomGui(payload)); + } catch (e: unknown) { + dispatch(createNotificationError('runCustomGuiAction', e as Error)); + } + }; +}; + +export type CustomGuiAction = + | RunCustomGuiAction; diff --git a/lib/static/modules/actions/filter-tests.ts b/lib/static/modules/actions/filter-tests.ts new file mode 100644 index 000000000..80e6ab3ae --- /dev/null +++ b/lib/static/modules/actions/filter-tests.ts @@ -0,0 +1,36 @@ +import actionNames from '@/static/modules/action-names'; +import type {Action} from '@/static/modules/actions/types'; +import {setFilteredBrowsers} from '@/static/modules/query-params'; +import {BrowserItem} from '@/types'; +import {ViewMode} from '@/constants'; + +export type UpdateTestNameFilterAction = Action; +export const updateTestNameFilter = (testNameFilter: UpdateTestNameFilterAction['payload']): UpdateTestNameFilterAction => { + return {type: actionNames.VIEW_UPDATE_FILTER_BY_NAME, payload: testNameFilter}; +}; + +export type SetStrictMatchFilterAction = Action; +export const setStrictMatchFilter = (strictMatchFilter: SetStrictMatchFilterAction['payload']): SetStrictMatchFilterAction => { + return {type: actionNames.VIEW_SET_STRICT_MATCH_FILTER, payload: strictMatchFilter}; +}; + +export type SelectBrowsersAction = Action; +export const selectBrowsers = (browsers: BrowserItem[]): SelectBrowsersAction => { + setFilteredBrowsers(browsers); + + return { + type: actionNames.BROWSERS_SELECTED, + payload: {browsers} + }; +}; + +export type ChangeViewModeAction = Action; +export const changeViewMode = (payload: ChangeViewModeAction['payload']): ChangeViewModeAction => ({type: actionNames.CHANGE_VIEW_MODE, payload}); + +export type FilterTestsAction = + | UpdateTestNameFilterAction + | SetStrictMatchFilterAction + | SelectBrowsersAction + | ChangeViewModeAction; diff --git a/lib/static/modules/actions/find-same-diffs.ts b/lib/static/modules/actions/find-same-diffs.ts new file mode 100644 index 000000000..3d7458f61 --- /dev/null +++ b/lib/static/modules/actions/find-same-diffs.ts @@ -0,0 +1,45 @@ +import {difference, isEmpty} from 'lodash'; +import axios from 'axios'; +import {types as modalTypes} from '@/static/components/modals'; +import {AppThunk} from '@/static/modules/actions/types'; +import {toggleLoading} from '@/static/modules/actions/loading'; +import {closeSections} from '@/static/modules/actions/suites-tree-state'; +import {openModal} from '@/static/modules/actions/modals'; + +export const thunkFindSameDiffs = (selectedImageId: string, openedImageIds: string[], browserName: string): AppThunk => { + return async (dispatch) => { + dispatch(toggleLoading({active: true, content: 'Find same diffs...'})); + + const comparedImageIds = openedImageIds.filter((id) => id.includes(browserName) && id !== selectedImageId); + let equalImagesIds = []; + + try { + if (!isEmpty(comparedImageIds)) { + const {data} = await axios.post('/get-find-equal-diffs-data', [selectedImageId].concat(comparedImageIds)); + + if (!isEmpty(data)) { + equalImagesIds = (await axios.post('/find-equal-diffs', data)).data; + } + } + + const closeImagesIds = difference(openedImageIds, ([] as string[]).concat(selectedImageId, equalImagesIds)); + + if (!isEmpty(closeImagesIds)) { + dispatch(closeSections(closeImagesIds)); + } + } catch (e) { + console.error('Error while trying to find equal diffs:', e); + } finally { + dispatch(toggleLoading({active: false})); + dispatch(openModal({ + id: modalTypes.FIND_SAME_DIFFS, + type: modalTypes.FIND_SAME_DIFFS, + data: { + browserId: browserName, + equalImages: equalImagesIds.length, + comparedImages: comparedImageIds.length + } + })); + } + }; +}; diff --git a/lib/static/modules/actions/group-tests.ts b/lib/static/modules/actions/group-tests.ts index 6a4443706..489339ea1 100644 --- a/lib/static/modules/actions/group-tests.ts +++ b/lib/static/modules/actions/group-tests.ts @@ -1,11 +1,16 @@ import actionNames from '@/static/modules/action-names'; import {Action} from '@/static/modules/actions/types'; +/** This action is used in old UI only */ +type GroupTestsByKeyAction = Action; +export const groupTestsByKey = (payload: string | undefined): GroupTestsByKeyAction => ({type: actionNames.GROUP_TESTS_BY_KEY, payload}); + type SetCurrentGroupByExpressionAction = Action; - export const setCurrentGroupByExpression = (payload: SetCurrentGroupByExpressionAction['payload']): SetCurrentGroupByExpressionAction => ({type: actionNames.GROUP_TESTS_SET_CURRENT_EXPRESSION, payload}); -export type GroupTestsAction = SetCurrentGroupByExpressionAction; +export type GroupTestsAction = + | SetCurrentGroupByExpressionAction + | GroupTestsByKeyAction; diff --git a/lib/static/modules/actions/index.js b/lib/static/modules/actions/index.js deleted file mode 100644 index a3a05c9f0..000000000 --- a/lib/static/modules/actions/index.js +++ /dev/null @@ -1,273 +0,0 @@ -import axios from 'axios'; -import {isEmpty, difference} from 'lodash'; -import {notify, dismissNotification as dismissNotify, POSITIONS} from 'reapop'; -import actionNames from '../action-names'; -import {types as modalTypes} from '../../components/modals'; -import {QUEUED} from '../../../constants/test-statuses'; -import {DiffModes} from '../../../constants/diff-modes'; -import {getHttpErrorMessage} from '../utils'; -import {connectToDatabase, getMainDatabaseUrl} from '../../../db-utils/client'; -import {setFilteredBrowsers} from '../query-params'; - -export * from './lifecycle'; -export * from './group-tests'; -export * from './static-accepter'; -export * from './suites-page'; -export * from './suites'; -export * from './visual-checks-page'; - -export const createNotification = (id, status, message, props = {}) => { - const notificationProps = { - position: POSITIONS.topCenter, - dismissAfter: 5000, - dismissible: true, - showDismissButton: true, - allowHTML: true, - ...props - }; - - return notify({id, status, message, ...notificationProps}); -}; - -export const createNotificationError = (id, error, props = {dismissAfter: 0}) => - createNotification(id, 'error', getHttpErrorMessage(error), props); - -export const dismissNotification = dismissNotify; - -const runTests = ({tests = [], action = {}} = {}) => { - return async (dispatch) => { - try { - dispatch(action); - await axios.post('/run', tests); - } catch (e) { - console.error('Error while running tests:', e); - } - }; -}; - -export const runAllTests = () => { - return runTests({ - action: { - type: actionNames.RUN_ALL_TESTS, - payload: {status: QUEUED} - } - }); -}; - -export const runFailedTests = (failedTests, actionName = actionNames.RUN_FAILED_TESTS) => { - return runTests({tests: failedTests, action: {type: actionName}}); -}; - -export const retrySuite = (tests) => { - return runTests({tests, action: {type: actionNames.RETRY_SUITE}}); -}; - -export const retryTest = (test) => { - return runTests({tests: [test], action: {type: actionNames.RETRY_TEST}}); -}; - -export const openModal = (payload) => ({type: actionNames.OPEN_MODAL, payload}); -export const closeModal = (payload) => ({type: actionNames.CLOSE_MODAL, payload}); - -export const acceptOpened = (imageIds, type = actionNames.ACCEPT_OPENED_SCREENSHOTS) => { - return async (dispatch) => { - dispatch({type: actionNames.PROCESS_BEGIN}); - - try { - const {data} = await axios.post('/reference-data-to-update', imageIds); - const {data: updatedData} = await axios.post('/update-reference', data); - dispatch({type, payload: updatedData}); - - return updatedData; - } catch (e) { - console.error('Error while updating references of failed tests:', e); - dispatch(createNotificationError('acceptScreenshot', e)); - - return null; - } finally { - dispatch({type: actionNames.PROCESS_END}); - } - }; -}; - -export const acceptTest = (imageId) => { - return acceptOpened([imageId], actionNames.ACCEPT_SCREENSHOT); -}; - -export const screenshotAccepterAccept = (imageId) => { - return acceptOpened([imageId], actionNames.SCREENSHOT_ACCEPTER_ACCEPT); -}; - -export const applyDelayedTestResults = (testResults) => { - return {type: actionNames.APPLY_DELAYED_TEST_RESULTS, payload: testResults}; -}; - -export const undoAcceptImages = (imageIds, {skipTreeUpdate = false} = {}) => { - return async (dispatch) => { - dispatch({type: actionNames.PROCESS_BEGIN}); - - try { - const {data} = await axios.post('/reference-data-to-update', imageIds); - const {data: updatedData} = await axios.post('/undo-accept-images', data); - const payload = {...updatedData, skipTreeUpdate}; - - dispatch({type: actionNames.UNDO_ACCEPT_IMAGES, payload}); - } catch (e) { - console.error('Error while reverting reference:', e); - dispatch(createNotificationError('undoScreenshot', e)); - } finally { - dispatch({type: actionNames.PROCESS_END}); - } - }; -}; - -export const undoAcceptImage = (imageId, opts) => undoAcceptImages([imageId], opts); - -export const stopTests = () => async dispatch => { - try { - await axios.post('/stop'); - dispatch({type: actionNames.STOP_TESTS}); - } catch (e) { - console.error('Error while stopping tests:', e); - } -}; - -export const testsEnd = () => async dispatch => { - try { - const mainDatabaseUrl = getMainDatabaseUrl(); - const db = await connectToDatabase(mainDatabaseUrl.href); - - dispatch({ - type: actionNames.TESTS_END, - payload: {db} - }); - } catch (e) { - dispatch(createNotificationError('testsEnd', e)); - } -}; - -export const suiteBegin = (suite) => ({type: actionNames.SUITE_BEGIN, payload: suite}); -export const testBegin = (test) => ({type: actionNames.TEST_BEGIN, payload: test}); -export const testResult = (result) => ({type: actionNames.TEST_RESULT, payload: result}); -export const toggleStateResult = (result) => ({type: actionNames.TOGGLE_STATE_RESULT, payload: result}); -export const toggleLoading = (payload) => ({type: actionNames.TOGGLE_LOADING, payload}); -export const closeSections = (payload) => ({type: actionNames.CLOSE_SECTIONS, payload}); -export const runFailed = () => ({type: actionNames.RUN_FAILED_TESTS}); -export const expandAll = () => ({type: actionNames.VIEW_EXPAND_ALL}); -export const selectAll = () => ({type: actionNames.SELECT_ALL}); -export const deselectAll = () => ({type: actionNames.DESELECT_ALL}); -export const expandErrors = () => ({type: actionNames.VIEW_EXPAND_ERRORS}); -export const expandRetries = () => ({type: actionNames.VIEW_EXPAND_RETRIES}); -export const collapseAll = () => ({type: actionNames.VIEW_COLLAPSE_ALL}); -export const processBegin = () => ({type: actionNames.PROCESS_BEGIN}); -export const processEnd = () => ({type: actionNames.PROCESS_END}); -export const updateBaseHost = (host) => ({type: actionNames.VIEW_UPDATE_BASE_HOST, host}); -export const copySuiteName = (payload) => ({type: actionNames.COPY_SUITE_NAME, payload}); -export const viewInBrowser = () => ({type: actionNames.VIEW_IN_BROWSER}); -export const copyTestLink = () => ({type: actionNames.COPY_TEST_LINK}); -export const toggleSuiteSection = (payload) => ({type: actionNames.TOGGLE_SUITE_SECTION, payload}); -export const toggleBrowserSection = (payload) => ({type: actionNames.TOGGLE_BROWSER_SECTION, payload}); -export const toggleMetaInfo = () => ({type: actionNames.TOGGLE_META_INFO}); -export const togglePageScreenshot = () => ({type: actionNames.TOGGLE_PAGE_SCREENSHOT}); -export const toggleBrowserCheckbox = (payload) => ({type: actionNames.TOGGLE_BROWSER_CHECKBOX, payload}); -export const toggleSuiteCheckbox = (payload) => ({type: actionNames.TOGGLE_SUITE_CHECKBOX, payload}); -export const toggleGroupCheckbox = (payload) => ({type: actionNames.TOGGLE_GROUP_CHECKBOX, payload}); -export const updateBottomProgressBar = (payload) => ({type: actionNames.UPDATE_BOTTOM_PROGRESS_BAR, payload}); -export const toggleTestsGroup = (payload) => ({type: actionNames.TOGGLE_TESTS_GROUP, payload}); -export const groupTestsByKey = (payload) => ({type: actionNames.GROUP_TESTS_BY_KEY, payload}); -export const changeViewMode = (payload) => ({type: actionNames.CHANGE_VIEW_MODE, payload}); - -export const runCustomGuiAction = (payload) => { - return async (dispatch) => { - try { - const {sectionName, groupIndex, controlIndex} = payload; - - await axios.post('/run-custom-gui-action', {sectionName, groupIndex, controlIndex}); - - dispatch({type: actionNames.RUN_CUSTOM_GUI_ACTION, payload}); - } catch (e) { - dispatch(createNotificationError('runCustomGuiAction', e)); - } - }; -}; - -export const updateTestNameFilter = (testNameFilter) => { - return {type: actionNames.VIEW_UPDATE_FILTER_BY_NAME, testNameFilter}; -}; - -export const setStrictMatchFilter = (strictMatchFilter) => { - return {type: actionNames.VIEW_SET_STRICT_MATCH_FILTER, strictMatchFilter}; -}; - -export function changeDiffMode(mode) { - switch (mode) { - case DiffModes.ONLY_DIFF.id: - return {type: actionNames.VIEW_ONLY_DIFF}; - - case DiffModes.SWITCH.id: - return {type: actionNames.VIEW_SWITCH_DIFF}; - - case DiffModes.SWIPE.id: - return {type: actionNames.VIEW_SWIPE_DIFF}; - - case DiffModes.ONION_SKIN.id: - return {type: actionNames.VIEW_ONION_SKIN_DIFF}; - - case DiffModes.THREE_UP_SCALED.id: - return {type: actionNames.VIEW_THREE_UP_SCALED_DIFF}; - - case DiffModes.THREE_UP_SCALED_TO_FIT.id: - return {type: actionNames.VIEW_THREE_UP_SCALED_TO_FIT_DIFF}; - - case DiffModes.THREE_UP.id: - default: - return {type: actionNames.VIEW_THREE_UP_DIFF}; - } -} - -export const findSameDiffs = (selectedImageId, openedImageIds, browserName) => { - return async (dispatch) => { - dispatch(toggleLoading({active: true, content: 'Find same diffs...'})); - - const comparedImageIds = openedImageIds.filter((id) => id.includes(browserName) && id !== selectedImageId); - let equalImagesIds = []; - - try { - if (!isEmpty(comparedImageIds)) { - const {data} = await axios.post('/get-find-equal-diffs-data', [selectedImageId].concat(comparedImageIds)); - - if (!isEmpty(data)) { - equalImagesIds = (await axios.post('/find-equal-diffs', data)).data; - } - } - - const closeImagesIds = difference(openedImageIds, [].concat(selectedImageId, equalImagesIds)); - - if (!isEmpty(closeImagesIds)) { - dispatch(closeSections(closeImagesIds)); - } - } catch (e) { - console.error('Error while trying to find equal diffs:', e); - } finally { - dispatch(toggleLoading({active: false})); - dispatch(openModal({ - id: modalTypes.FIND_SAME_DIFFS, - type: modalTypes.FIND_SAME_DIFFS, - data: { - browserId: browserName, - equalImages: equalImagesIds.length, - comparedImages: comparedImageIds.length - } - })); - } - }; -}; - -export const selectBrowsers = (browsers) => { - setFilteredBrowsers(browsers); - - return { - type: actionNames.BROWSERS_SELECTED, - payload: {browsers} - }; -}; diff --git a/lib/static/modules/actions/index.ts b/lib/static/modules/actions/index.ts new file mode 100644 index 000000000..68747cfe7 --- /dev/null +++ b/lib/static/modules/actions/index.ts @@ -0,0 +1,18 @@ +export * from './custom-gui'; +export * from './filter-tests'; +export * from './find-same-diffs'; +export * from './group-tests'; +export * from './gui-server-connection'; +export * from './lifecycle'; +export * from './loading'; +export * from './modals'; +export * from './notifications'; +export * from './processing'; +export * from './run-tests'; +export * from './screenshots'; +export * from './settings'; +export * from './sort-tests'; +export * from './static-accepter'; +export * from './suites-page'; +export * from './suites-tree-state'; +export * from './visual-checks-page'; diff --git a/lib/static/modules/actions/lifecycle.ts b/lib/static/modules/actions/lifecycle.ts index c620c0822..0957c3e92 100644 --- a/lib/static/modules/actions/lifecycle.ts +++ b/lib/static/modules/actions/lifecycle.ts @@ -12,12 +12,12 @@ import { import * as plugins from '@/static/modules/plugins'; import actionNames from '@/static/modules/action-names'; import {FinalStats, SkipItem, StaticTestsTreeBuilder} from '@/tests-tree-builder/static'; -import {createNotificationError} from '@/static/modules/actions/index'; import {Action, AppThunk} from '@/static/modules/actions/types'; import {DataForStaticFile} from '@/server-utils'; import {GetInitResponse} from '@/gui/server'; import {Tree} from '@/tests-tree-builder/base'; import {BrowserItem} from '@/types'; +import {createNotificationError} from '@/static/modules/actions/notifications'; export type InitGuiReportAction = Action; const initGuiReport = (payload: InitGuiReportAction['payload']): InitGuiReportAction => @@ -51,11 +51,11 @@ export const thunkInitGuiReport = ({isNewUi}: InitGuiReportData = {}): AppThunk if (appState.data.customGuiError) { const {customGuiError} = appState.data; - dispatch(createNotificationError('initGuiReport', {...customGuiError})); + dispatch(createNotificationError('initGuiReport', {name: 'CustomGuiError', message: customGuiError?.response.data})); delete appState.data.customGuiError; } - } catch (e) { - dispatch(createNotificationError('initGuiReport', e)); + } catch (e: unknown) { + dispatch(createNotificationError('initGuiReport', e as Error)); } }; }; @@ -105,8 +105,8 @@ export const thunkInitStaticReport = ({isNewUi}: InitStaticReportData = {}): App db = await mergeDatabases(dataForDbs); performance?.mark?.(performanceMarks.DBS_MERGED); - } catch (e) { - dispatch(createNotificationError('thunkInitStaticReport', e)); + } catch (e: unknown) { + dispatch(createNotificationError('thunkInitStaticReport', e as Error)); } await plugins.loadAll(dataFromStaticFile.config); diff --git a/lib/static/modules/actions/loading.ts b/lib/static/modules/actions/loading.ts new file mode 100644 index 000000000..6c3a69cbc --- /dev/null +++ b/lib/static/modules/actions/loading.ts @@ -0,0 +1,11 @@ +import actionNames from '@/static/modules/action-names'; +import type {Action} from '@/static/modules/actions/types'; + +export type ToggleLoadingAction = Action; +export const toggleLoading = (payload: ToggleLoadingAction['payload']): ToggleLoadingAction => ({type: actionNames.TOGGLE_LOADING, payload}); + +export type LoadingAction = + | ToggleLoadingAction; diff --git a/lib/static/modules/actions/modals.ts b/lib/static/modules/actions/modals.ts new file mode 100644 index 000000000..3cbd1672a --- /dev/null +++ b/lib/static/modules/actions/modals.ts @@ -0,0 +1,18 @@ +import type {Action} from '@/static/modules/actions/types'; +import actionNames from '@/static/modules/action-names'; + +export type OpenModalAction = Action; +export const openModal = (payload: OpenModalAction['payload']): OpenModalAction => ({type: actionNames.OPEN_MODAL, payload}); + +export type CloseModalAction = Action; +export const closeModal = (payload: CloseModalAction['payload']): CloseModalAction => ({type: actionNames.CLOSE_MODAL, payload}); + +export type ModalsAction = + | OpenModalAction + | CloseModalAction; diff --git a/lib/static/modules/actions/notifications.ts b/lib/static/modules/actions/notifications.ts new file mode 100644 index 000000000..ad3b1fccc --- /dev/null +++ b/lib/static/modules/actions/notifications.ts @@ -0,0 +1,27 @@ +import { + notify, + POSITIONS, + Status as NotificationStatus, Notification +} from 'reapop'; + +import {getHttpErrorMessage} from '@/static/modules/utils'; + +type UpsertNotificationAction = ReturnType; + +export const createNotification = (id: string, status: NotificationStatus, message: string, props: Partial = {}): UpsertNotificationAction => { + const notificationProps: Partial = { + position: POSITIONS.topCenter, + dismissAfter: 5000, + dismissible: true, + showDismissButton: true, + allowHTML: true, + ...props + }; + + return notify({id, status, message, ...notificationProps}); +}; + +export const createNotificationError = (id: string, error: Error, props: Partial = {dismissAfter: 0}): UpsertNotificationAction => + createNotification(id, 'error', getHttpErrorMessage(error), props); + +export {dismissNotification} from 'reapop'; diff --git a/lib/static/modules/actions/processing.ts b/lib/static/modules/actions/processing.ts new file mode 100644 index 000000000..12e0db7c5 --- /dev/null +++ b/lib/static/modules/actions/processing.ts @@ -0,0 +1,12 @@ +import actionNames from '@/static/modules/action-names'; +import {Action} from '@/static/modules/actions/types'; + +export type ProcessBeginAction = Action; +export const processBegin = (): ProcessBeginAction => ({type: actionNames.PROCESS_BEGIN}); + +export type ProcessEndAction = Action; +export const processEnd = (): ProcessEndAction => ({type: actionNames.PROCESS_END}); + +export type ProcessingAction = + | ProcessBeginAction + | ProcessEndAction; diff --git a/lib/static/modules/actions/run-tests.ts b/lib/static/modules/actions/run-tests.ts new file mode 100644 index 000000000..349d39f89 --- /dev/null +++ b/lib/static/modules/actions/run-tests.ts @@ -0,0 +1,116 @@ +import axios from 'axios'; + +import actionNames from '@/static/modules/action-names'; +import {Action, AppThunk} from '@/static/modules/actions/types'; +import {TestSpec} from '@/adapters/tool/types'; +import {connectToDatabase, getMainDatabaseUrl} from '@/db-utils/client'; +import {createNotificationError} from '@/static/modules/actions/notifications'; +import {TestBranch} from '@/tests-tree-builder/gui'; +import {TestStatus} from '@/constants'; + +export const thunkRunTests = ({tests = []}: {tests?: TestSpec[]} = {}): AppThunk => { + return async () => { + try { + await axios.post('/run', tests); + } catch (e) { + // TODO: report error via notifications + console.error('Error while running tests:', e); + } + }; +}; + +export type RunAllTestsAction = Action; +export const runAllTests = (): RunAllTestsAction => ({type: actionNames.RUN_ALL_TESTS}); + +export const thunkRunAllTests = (): AppThunk => { + return async (dispatch) => { + dispatch(runAllTests()); + await dispatch(thunkRunTests()); + }; +}; + +export type RunFailedTestsAction = Action; +export const runFailedTests = (): RunFailedTestsAction => ({type: actionNames.RUN_FAILED_TESTS}); + +export const thunkRunFailedTests = ({tests}: {tests: TestSpec[]}): AppThunk => { + return async (dispatch) => { + dispatch(runFailedTests()); + await dispatch(thunkRunTests({tests})); + }; +}; + +export type RunSuiteAction = Action; +export const runSuite = (): RunSuiteAction => ({type: actionNames.RETRY_SUITE}); + +export const thunkRunSuite = ({tests}: {tests: TestSpec[]}): AppThunk => { + return async (dispatch) => { + dispatch(runSuite()); + await dispatch(thunkRunTests({tests})); + }; +}; + +export type RunTestAction = Action; +export const runTest = (): RunTestAction => ({type: actionNames.RETRY_TEST}); + +export const thunkRunTest = ({test}: {test: TestSpec}): AppThunk => { + return async (dispatch) => { + dispatch(runTest()); + await dispatch(thunkRunTests({tests: [test]})); + }; +}; + +export type StopTestsAction = Action; +export const stopTests = (): StopTestsAction => ({type: actionNames.STOP_TESTS}); + +export const thunkStopTests = (): AppThunk => { + return async (dispatch) => { + try { + await axios.post('/stop'); + dispatch(stopTests()); + } catch (e) { + // TODO: report error via notifications + console.error('Error while stopping tests:', e); + } + }; +}; + +export type TestsEndAction = Action; +export const testsEnd = (payload: TestsEndAction['payload']): TestsEndAction => ({type: actionNames.TESTS_END, payload}); + +export const thunkTestsEnd = (): AppThunk => { + return async (dispatch) => { + try { + const mainDatabaseUrl = getMainDatabaseUrl(); + const db = await connectToDatabase(mainDatabaseUrl.href); + + dispatch(testsEnd({db})); + } catch (e: unknown) { + dispatch(createNotificationError('testsEnd', e as Error)); + } + }; +}; + +export type SuiteBeginAction = Action; +export const suiteBegin = (payload: SuiteBeginAction['payload']): SuiteBeginAction => ({type: actionNames.SUITE_BEGIN, payload}); + +export type TestBeginAction = Action; +export const testBegin = (payload: TestBeginAction['payload']): TestBeginAction => ({type: actionNames.TEST_BEGIN, payload}); + +export type TestResultAction = Action; +export const testResult = (payload: TestResultAction['payload']): TestResultAction => ({type: actionNames.TEST_RESULT, payload}); + +export type RunTestsAction = + | RunAllTestsAction + | RunFailedTestsAction + | RunSuiteAction + | RunTestAction + | StopTestsAction + | TestsEndAction + | SuiteBeginAction + | TestBeginAction + | TestResultAction; diff --git a/lib/static/modules/actions/screenshots.ts b/lib/static/modules/actions/screenshots.ts new file mode 100644 index 000000000..b8c97671c --- /dev/null +++ b/lib/static/modules/actions/screenshots.ts @@ -0,0 +1,74 @@ +import axios from 'axios'; + +import actionNames from '@/static/modules/action-names'; +import {Action, AppThunk} from '@/static/modules/actions/types'; +import {processBegin, processEnd} from '@/static/modules/actions/processing'; +import {TestBranch, TestRefUpdateData} from '@/tests-tree-builder/gui'; +import {UndoAcceptImagesResult} from '@/gui/tool-runner'; +import {createNotificationError} from '@/static/modules/actions/notifications'; + +export type CommitAcceptedImagesToTreeAction = Action; +export const commitAcceptedImagesToTree = (payload: CommitAcceptedImagesToTreeAction['payload']): CommitAcceptedImagesToTreeAction => ({type: actionNames.COMMIT_ACCEPTED_IMAGES_TO_TREE, payload}); + +interface AcceptImagesData { + imageIds: string[]; + shouldCommitUpdatesToTree?: boolean; +} + +export const thunkAcceptImages = ({imageIds, shouldCommitUpdatesToTree = true}: AcceptImagesData): AppThunk> => { + return async (dispatch) => { + dispatch(processBegin()); + + try { + const {data} = await axios.post('/reference-data-to-update', imageIds); + const {data: testBranches} = await axios.post('/update-reference', data); + if (shouldCommitUpdatesToTree) { + dispatch(commitAcceptedImagesToTree(testBranches)); + } + + dispatch(processEnd()); + + return testBranches; + } catch (e: unknown) { + console.error('Error while updating references of failed tests:', e); + dispatch(createNotificationError('acceptScreenshot', e as Error)); + + dispatch(processEnd()); + + return null; + } + }; +}; + +export type CommitRevertedImagesToTreeAction = Action; +export const commitRevertedImagesToTree = (payload: CommitRevertedImagesToTreeAction['payload']): CommitRevertedImagesToTreeAction => ({type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, payload}); + +interface RevertImagesData { + imageIds: string[]; + shouldCommitUpdatesToTree?: boolean; +} + +export const thunkRevertImages = ({imageIds, shouldCommitUpdatesToTree = true}: RevertImagesData): AppThunk => { + return async (dispatch) => { + dispatch(processBegin()); + + try { + const {data} = await axios.post('/reference-data-to-update', imageIds); + const {data: updatedData} = await axios.post('/undo-accept-images', data); + if (shouldCommitUpdatesToTree) { + dispatch(commitRevertedImagesToTree(updatedData)); + } + + dispatch(processEnd()); + } catch (e: unknown) { + console.error('Error while reverting reference:', e); + dispatch(createNotificationError('undoScreenshot', e as Error)); + + dispatch(processEnd()); + } + }; +}; + +export type ScreenshotsAction = + | CommitAcceptedImagesToTreeAction + | CommitRevertedImagesToTreeAction; diff --git a/lib/static/modules/actions/settings.ts b/lib/static/modules/actions/settings.ts new file mode 100644 index 000000000..c20945812 --- /dev/null +++ b/lib/static/modules/actions/settings.ts @@ -0,0 +1,17 @@ +import {DiffModeId} from '@/constants'; +import {Action} from '@/static/modules/actions/types'; +import actionNames from '@/static/modules/action-names'; + +type UpdateBaseHostAction = Action; +export const updateBaseHost = (host: string): UpdateBaseHostAction => ({type: actionNames.VIEW_UPDATE_BASE_HOST, payload: {host}}); + +type SetDiffModeAction = Action; +export const setDiffMode = (payload: SetDiffModeAction['payload']): SetDiffModeAction => ({type: actionNames.SET_DIFF_MODE, payload}); + +export type SettingsAction = + | UpdateBaseHostAction + | SetDiffModeAction; diff --git a/lib/static/modules/actions/static-accepter.ts b/lib/static/modules/actions/static-accepter.ts index 1580edb71..a89cc03fd 100644 --- a/lib/static/modules/actions/static-accepter.ts +++ b/lib/static/modules/actions/static-accepter.ts @@ -1,7 +1,6 @@ import axios from 'axios'; import {isEmpty} from 'lodash'; import {types as modalTypes} from '../../components/modals'; -import {openModal, closeModal, createNotificationError, createNotification} from './index'; import {getBlob} from '../utils'; import {storeCommitInLocalStorage} from '../static-image-accepter'; import actionNames from '../action-names'; @@ -9,6 +8,8 @@ import defaultState from '../default-state'; import type {Action, Dispatch, Store} from './types'; import {ThunkAction} from 'redux-thunk'; import {Point} from '@/static/new-ui/types'; +import {closeModal, openModal} from '@/static/modules/actions/modals'; +import {createNotification, createNotificationError} from '@/static/modules/actions/notifications'; type StaticAccepterDelayScreenshotPayload = {imageId: string, stateName: string, stateNameImageId: string}[]; type StaticAccepterDelayScreenshotAction = Action @@ -122,7 +123,7 @@ export const staticAccepterCommitScreenshot = ( } } catch (e) { console.error('An error occurred while commiting screenshot:', e); - dispatch(createNotificationError('commitScreenshot', e)); + dispatch(createNotificationError('commitScreenshot', e as Error)); return {error: e as Error}; } finally { @@ -143,3 +144,12 @@ type StaticAccepterUpdateCommitMessageAction = Action { return {type: actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE, payload}; }; + +export type StaticAccepter = + | StaticAccepterDelayScreenshotAction + | StaticAccepterUndoDelayScreenshotAction + | StaticAccepterStageScreenshotAction + | StaticAccepterUnstageScreenshotAction + | StaticAccepterCommitScreenshotAction + | StaticAccepterUpdateToolbarPositionAction + | StaticAccepterUpdateCommitMessageAction; diff --git a/lib/static/modules/actions/suites-tree-state.ts b/lib/static/modules/actions/suites-tree-state.ts new file mode 100644 index 000000000..7229fa533 --- /dev/null +++ b/lib/static/modules/actions/suites-tree-state.ts @@ -0,0 +1,102 @@ +import actionNames from '@/static/modules/action-names'; +import {Action} from '@/static/modules/actions/types'; +import {CheckStatus} from '@/constants/checked-statuses'; + +/** These actions are mostly used in old UI */ + +export type ToggleStateResultAction = Action; +export const toggleStateResult = (payload: ToggleStateResultAction['payload']): ToggleStateResultAction => ({type: actionNames.TOGGLE_STATE_RESULT, payload}); + +export type CloseSectionsAction = Action; +export const closeSections = (payload: CloseSectionsAction['payload']): CloseSectionsAction => ({type: actionNames.CLOSE_SECTIONS, payload}); + +export type SelectAllAction = Action; +export const selectAll = (): SelectAllAction => ({type: actionNames.SELECT_ALL}); + +export type DeselectAllAction = Action; +export const deselectAll = (): DeselectAllAction => ({type: actionNames.DESELECT_ALL}); + +export type ExpandAllAction = Action; +export const expandAll = (): ExpandAllAction => ({type: actionNames.VIEW_EXPAND_ALL}); + +export type ExpandErrorsAction = Action; +export const expandErrors = (): ExpandErrorsAction => ({type: actionNames.VIEW_EXPAND_ERRORS}); + +export type ExpandRetriesAction = Action; +export const expandRetries = (): ExpandRetriesAction => ({type: actionNames.VIEW_EXPAND_RETRIES}); + +export type CollapseAllAction = Action; +export const collapseAll = (): CollapseAllAction => ({type: actionNames.VIEW_COLLAPSE_ALL}); + +export type ToggleSuiteSectionAction = Action; +export const toggleSuiteSection = (payload: ToggleSuiteSectionAction['payload']): ToggleSuiteSectionAction => ({type: actionNames.TOGGLE_SUITE_SECTION, payload}); + +export type ToggleBrowserSectionAction = Action; +export const toggleBrowserSection = (payload: ToggleBrowserSectionAction['payload']): ToggleBrowserSectionAction => ({type: actionNames.TOGGLE_BROWSER_SECTION, payload}); + +export type ToggleBrowserCheckboxAction = Action; +export const toggleBrowserCheckbox = (payload: ToggleBrowserCheckboxAction['payload']): ToggleBrowserCheckboxAction => ({type: actionNames.TOGGLE_BROWSER_CHECKBOX, payload}); + +export type ToggleSuiteCheckboxAction = Action; +export const toggleSuiteCheckbox = (payload: ToggleSuiteCheckboxAction['payload']): ToggleSuiteCheckboxAction => ({type: actionNames.TOGGLE_SUITE_CHECKBOX, payload}); + +export type ToggleGroupCheckboxAction = Action; +export const toggleGroupCheckbox = (payload: ToggleGroupCheckboxAction['payload']): ToggleGroupCheckboxAction => ({type: actionNames.TOGGLE_GROUP_CHECKBOX, payload}); + +export type UpdateBottomProgressBarAction = Action; +export const updateBottomProgressBar = (payload: UpdateBottomProgressBarAction['payload']): UpdateBottomProgressBarAction => ({type: actionNames.UPDATE_BOTTOM_PROGRESS_BAR, payload}); + +export type ToggleTestsGroupAction = Action; +export const toggleTestsGroup = (payload: ToggleTestsGroupAction['payload']): ToggleTestsGroupAction => ({type: actionNames.TOGGLE_TESTS_GROUP, payload}); + +export type ChangeTestRetryAction = Action; +export const changeTestRetry = (result: ChangeTestRetryAction['payload']): ChangeTestRetryAction => + ({type: actionNames.CHANGE_TEST_RETRY, payload: result}); + +export type SuiteTreeStateAction = + | ToggleStateResultAction + | CloseSectionsAction + | ExpandAllAction + | SelectAllAction + | DeselectAllAction + | ExpandErrorsAction + | ExpandRetriesAction + | CollapseAllAction + | ToggleSuiteSectionAction + | ToggleBrowserSectionAction + | ToggleBrowserCheckboxAction + | ToggleSuiteCheckboxAction + | ToggleGroupCheckboxAction + | UpdateBottomProgressBarAction + | ToggleTestsGroupAction + | ChangeTestRetryAction; diff --git a/lib/static/modules/actions/suites.ts b/lib/static/modules/actions/suites.ts deleted file mode 100644 index 7057864a9..000000000 --- a/lib/static/modules/actions/suites.ts +++ /dev/null @@ -1,13 +0,0 @@ -import actionNames from '@/static/modules/action-names'; -import {Action} from '@/static/modules/actions/types'; - -interface ChangeTestRetryPayload { - browserId: string; - retryIndex: number; - suitesPage?: { - treeNodeId: string; - } -} - -export const changeTestRetry = (result: ChangeTestRetryPayload): Action => - ({type: actionNames.CHANGE_TEST_RETRY, payload: result}); diff --git a/lib/static/modules/actions/types.ts b/lib/static/modules/actions/types.ts index 55f9700a4..ca66bd724 100644 --- a/lib/static/modules/actions/types.ts +++ b/lib/static/modules/actions/types.ts @@ -1,16 +1,26 @@ -import type actionNames from '../action-names'; import type {Action as ReduxAction} from 'redux'; -import type defaultState from '../default-state'; -import type {Tree} from '../../../tests-tree-builder/base'; -import {GroupTestsAction} from '@/static/modules/actions/group-tests'; +export type {Dispatch} from 'redux'; import {ThunkAction} from 'redux-thunk'; + +import {GroupTestsAction} from '@/static/modules/actions/group-tests'; import {State} from '@/static/new-ui/types/store'; import {LifecycleAction} from '@/static/modules/actions/lifecycle'; import {SuitesPageAction} from '@/static/modules/actions/suites-page'; import {SortTestsAction} from '@/static/modules/actions/sort-tests'; import {GuiServerConnectionAction} from '@/static/modules/actions/gui-server-connection'; - -export type {Dispatch} from 'redux'; +import {ScreenshotsAction} from '@/static/modules/actions/screenshots'; +import {RunTestsAction} from '@/static/modules/actions/run-tests'; +import {SuiteTreeStateAction} from '@/static/modules/actions/suites-tree-state'; +import {ModalsAction} from '@/static/modules/actions/modals'; +import {LoadingAction} from '@/static/modules/actions/loading'; +import {CustomGuiAction} from '@/static/modules/actions/custom-gui'; +import {FilterTestsAction} from '@/static/modules/actions/filter-tests'; +import {SettingsAction} from '@/static/modules/actions/settings'; +import {ProcessingAction} from '@/static/modules/actions/processing'; +import {StaticAccepter} from '@/static/modules/actions/static-accepter'; +import type actionNames from '../action-names'; +import type defaultState from '../default-state'; +import type {Tree} from '../../../tests-tree-builder/base'; export type Store = Omit & {tree: Tree}; @@ -22,8 +32,18 @@ export type Action< export type AppThunk> = ThunkAction; export type SomeAction = + | CustomGuiAction + | FilterTestsAction | GroupTestsAction + | GuiServerConnectionAction | LifecycleAction + | LoadingAction + | ModalsAction + | ProcessingAction + | RunTestsAction + | ScreenshotsAction + | SettingsAction | SortTestsAction + | StaticAccepter | SuitesPageAction - | GuiServerConnectionAction; + | SuiteTreeStateAction; diff --git a/lib/static/modules/middlewares/metrika.js b/lib/static/modules/middlewares/metrika.js deleted file mode 100644 index 2da1a7017..000000000 --- a/lib/static/modules/middlewares/metrika.js +++ /dev/null @@ -1,154 +0,0 @@ -import {get, isEmpty} from 'lodash'; -import actionNames from '../action-names'; - -import {measurePerformance} from '../web-vitals'; -import performanceMarks from '../../../constants/performance-marks'; - -let metrika; -let reportFullyLoaded = false; - -export default metrikaClass => store => next => action => { - switch (action.type) { - case actionNames.INIT_GUI_REPORT: - case actionNames.INIT_STATIC_REPORT: { - const startLoadTime = Date.now(); - - const {config: pluginConfig} = action.payload; - const ymConfig = get(pluginConfig, 'yandexMetrika', {}); - - if (!ymConfig.counterNumber) { - return next(action); - } - - metrika = metrikaClass.create(ymConfig); - - measurePerformance(({name, value}) => { - const intValue = Math.round(name === 'CLS' ? value * 1000 : value); - metrika.sendVisitParams({[name]: intValue}); - }); - - const result = next(action); - const state = store.getState(); - const testsCount = get(state, 'tree.browsers.allIds.length', 0); - - metrika.sendVisitParams({ - [action.type]: Date.now() - startLoadTime, - initView: state.view, - testsCount - }); - - return result; - } - - case actionNames.ACCEPT_SCREENSHOT: - case actionNames.SCREENSHOT_ACCEPTER_ACCEPT: { - execOnMetrikaEnabled(() => { - metrika.acceptScreenshot(); - sendCounterId(action.type); - }); - - return next(action); - } - - case actionNames.ACCEPT_OPENED_SCREENSHOTS: { - execOnMetrikaEnabled(() => { - const payload = get(action, 'payload', []); - metrika.acceptOpenedScreenshots({acceptedImagesCount: payload.length}); - sendCounterId(action.type); - }); - - return next(action); - } - - case actionNames.BROWSERS_SELECTED: { - execOnMetrikaEnabled(() => { - sendCounterId(action.type); - }); - - const result = next(action); - - if (!reportFullyLoaded) { - reportFullyLoaded = true; - - performance?.mark?.(performanceMarks.FULLY_LOADED); - - const marks = extractPerformanceMarks(); - - if (metrika && !isEmpty(marks)) { - metrika.sendVisitParams(marks); - } - } - - return result; - } - - case actionNames.RUN_ALL_TESTS: - case actionNames.RUN_FAILED_TESTS: - case actionNames.RETRY_SUITE: - case actionNames.RETRY_TEST: - case actionNames.CHANGE_VIEW_MODE: - case actionNames.VIEW_EXPAND_ALL: - case actionNames.VIEW_COLLAPSE_ALL: - case actionNames.VIEW_EXPAND_ERRORS: - case actionNames.VIEW_EXPAND_RETRIES: - case actionNames.VIEW_UPDATE_BASE_HOST: - case actionNames.VIEW_THREE_UP_DIFF: - case actionNames.VIEW_THREE_UP_SCALED_DIFF: - case actionNames.VIEW_THREE_UP_SCALED_TO_FIT_DIFF: - case actionNames.VIEW_ONLY_DIFF: - case actionNames.VIEW_SWITCH_DIFF: - case actionNames.VIEW_SWIPE_DIFF: - case actionNames.VIEW_ONION_SKIN_DIFF: - case actionNames.VIEW_UPDATE_FILTER_BY_NAME: - case actionNames.VIEW_SET_STRICT_MATCH_FILTER: - case actionNames.RUN_CUSTOM_GUI_ACTION: - case actionNames.COPY_SUITE_NAME: - case actionNames.VIEW_IN_BROWSER: - case actionNames.COPY_TEST_LINK: - case actionNames.TOGGLE_SUITE_SECTION: - case actionNames.TOGGLE_BROWSER_SECTION: - case actionNames.TOGGLE_STATE_RESULT: - case actionNames.CHANGE_TEST_RETRY: - case actionNames.GROUP_TESTS_BY_KEY: - case actionNames.APPLY_DELAYED_TEST_RESULTS: { - execOnMetrikaEnabled(() => { - sendCounterId(action.type); - }); - - return next(action); - } - - case actionNames.OPEN_MODAL: - case actionNames.CLOSE_MODAL: { - execOnMetrikaEnabled(() => { - const modalId = get(action, 'payload.id', action.type); - sendCounterId(modalId); - }); - - return next(action); - } - - default: - return next(action); - } -}; - -function execOnMetrikaEnabled(cb) { - if (metrika) { - cb(); - } -} - -function sendCounterId(counterId) { - metrika.sendVisitParams({counterId}); -} - -function extractPerformanceMarks() { - const marks = performance?.getEntriesByType?.('mark') || []; - - return marks.reduce((acc, {name, startTime}) => { - acc[name] = Math.round(startTime); - - return acc; - }, {}); -} diff --git a/lib/static/modules/middlewares/metrika.ts b/lib/static/modules/middlewares/metrika.ts new file mode 100644 index 000000000..6788f2b84 --- /dev/null +++ b/lib/static/modules/middlewares/metrika.ts @@ -0,0 +1,97 @@ +import {get, isEmpty} from 'lodash'; +import {Middleware} from 'redux'; + +import type {YandexMetrika} from '@/static/modules/yandex-metrika'; +import type {State} from '@/static/new-ui/types/store'; +import {SomeAction} from '@/static/modules/actions/types'; +import actionNames from '../action-names'; +import {measurePerformance} from '../web-vitals'; +import performanceMarks from '../../../constants/performance-marks'; +import {extractPerformanceMarks} from '@/static/modules/utils/performance'; + +// This rule should be disabled here per redux docs: https://redux.js.org/usage/usage-with-typescript#type-checking-middleware +// eslint-disable-next-line @typescript-eslint/ban-types +export function getMetrikaMiddleware(analytics: YandexMetrika): Middleware<{}, State> { + let reportFullyLoaded = false; + + return store => next => (action: SomeAction) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: { + const startLoadTime = Date.now(); + + measurePerformance(({name, value}) => { + const intValue = Math.round(name === 'CLS' ? value * 1000 : value); + analytics.setVisitParams({[name]: intValue}); + }); + + const result = next(action); + const state = store.getState(); + const testsCount = get(state, 'tree.browsers.allIds.length', 0); + + analytics.setVisitParams({ + [action.type]: Date.now() - startLoadTime, + initView: state.view, + testsCount + }); + + return result; + } + + case actionNames.BROWSERS_SELECTED: { + analytics.trackFeatureUsage({featureName: action.type}); + + const result = next(action); + + if (!reportFullyLoaded) { + reportFullyLoaded = true; + + performance?.mark?.(performanceMarks.FULLY_LOADED); + + const marks = extractPerformanceMarks(); + + if (analytics && !isEmpty(marks)) { + analytics.setVisitParams(marks); + } + } + + return result; + } + + case actionNames.RUN_ALL_TESTS: + case actionNames.RUN_FAILED_TESTS: + case actionNames.RETRY_SUITE: + case actionNames.RETRY_TEST: + case actionNames.CHANGE_VIEW_MODE: + case actionNames.VIEW_EXPAND_ALL: + case actionNames.VIEW_COLLAPSE_ALL: + case actionNames.VIEW_EXPAND_ERRORS: + case actionNames.VIEW_EXPAND_RETRIES: + case actionNames.VIEW_UPDATE_BASE_HOST: + case actionNames.SET_DIFF_MODE: + case actionNames.VIEW_UPDATE_FILTER_BY_NAME: + case actionNames.VIEW_SET_STRICT_MATCH_FILTER: + case actionNames.RUN_CUSTOM_GUI_ACTION: + case actionNames.TOGGLE_SUITE_SECTION: + case actionNames.TOGGLE_BROWSER_SECTION: + case actionNames.TOGGLE_STATE_RESULT: + case actionNames.CHANGE_TEST_RETRY: + case actionNames.GROUP_TESTS_BY_KEY: { + analytics.trackFeatureUsage({featureName: action.type}); + + return next(action); + } + + case actionNames.OPEN_MODAL: + case actionNames.CLOSE_MODAL: { + const modalId = get(action, 'payload.id', action.type); + analytics.trackFeatureUsage({featureName: modalId}); + + return next(action); + } + + default: + return next(action); + } + }; +} diff --git a/lib/static/modules/reducers/grouped-tests/index.js b/lib/static/modules/reducers/grouped-tests/index.js index de2a98cf3..a4c002c41 100644 --- a/lib/static/modules/reducers/grouped-tests/index.js +++ b/lib/static/modules/reducers/grouped-tests/index.js @@ -15,9 +15,8 @@ export default (state, action) => { case actionNames.VIEW_UPDATE_FILTER_BY_NAME: case actionNames.VIEW_SET_STRICT_MATCH_FILTER: case actionNames.CHANGE_VIEW_MODE: - case actionNames.ACCEPT_SCREENSHOT: - case actionNames.ACCEPT_OPENED_SCREENSHOTS: - case actionNames.APPLY_DELAYED_TEST_RESULTS: { + case actionNames.COMMIT_ACCEPTED_IMAGES_TO_TREE: + case actionNames.COMMIT_REVERTED_IMAGES_TO_TREE: { const { tree, groupedTests, view: {keyToGroupTestsBy, viewMode, filteredBrowsers, testNameFilter, strictMatchFilter} diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index 9a6921350..539e25d31 100644 --- a/lib/static/modules/reducers/tree/index.js +++ b/lib/static/modules/reducers/tree/index.js @@ -15,7 +15,7 @@ import { } from './nodes/images'; import {ViewMode} from '../../../../constants/view-modes'; import {EXPAND_RETRIES} from '../../../../constants/expand-modes'; -import {COMMITED, FAIL, STAGED} from '../../../../constants/test-statuses'; +import {COMMITED, FAIL, STAGED, TestStatus} from '../../../../constants/test-statuses'; import {isCommitedStatus, isStagedStatus, isSuccessStatus} from '../../../../common-utils'; import {applyStateUpdate, ensureDiffProperty, getUpdatedProperty} from '../../utils/state'; import {changeNodeState, getStaticAccepterStateNameImages, resolveUpdatedStatuses, updateImagesStatus} from './helpers'; @@ -61,13 +61,12 @@ export default ((state, action) => { } case actionNames.RUN_ALL_TESTS: { - const {status} = action.payload; const {tree} = state; ensureDiffProperty(diff, ['tree', 'suites', 'byId']); tree.suites.allIds.forEach((suiteId) => { - diff.tree.suites.byId[suiteId] = {status}; + diff.tree.suites.byId[suiteId] = {status: TestStatus.QUEUED}; }); return applyStateUpdate(state, diff); @@ -342,21 +341,15 @@ export default ((state, action) => { } case actionNames.TEST_RESULT: - case actionNames.ACCEPT_OPENED_SCREENSHOTS: - case actionNames.ACCEPT_SCREENSHOT: - case actionNames.APPLY_DELAYED_TEST_RESULTS: { + case actionNames.COMMIT_ACCEPTED_IMAGES_TO_TREE: { addNodesToTree(draft, action.payload); break; } - case actionNames.UNDO_ACCEPT_IMAGES: { + case actionNames.COMMIT_REVERTED_IMAGES_TO_TREE: { const {tree, view} = draft; - const {updatedImages = [], removedResults = [], skipTreeUpdate} = action.payload; - - if (skipTreeUpdate) { - return; - } + const {updatedImages = [], removedResults = []} = action.payload; const failedRemovedResults = removedResults.filter(resultId => { const result = tree.results.byId[resultId]; diff --git a/lib/static/modules/reducers/view.js b/lib/static/modules/reducers/view.js index 1a5fdfc3e..1714c80e5 100644 --- a/lib/static/modules/reducers/view.js +++ b/lib/static/modules/reducers/view.js @@ -2,7 +2,6 @@ import {isEmpty} from 'lodash'; import {getViewQuery} from '../custom-queries'; import * as localStorageWrapper from '../local-storage-wrapper'; import actionNames from '../action-names'; -import {DiffModes} from '../../../constants/diff-modes'; import {EXPAND_ALL, COLLAPSE_ALL, EXPAND_ERRORS, EXPAND_RETRIES} from '../../../constants/expand-modes'; export default (state, action) => { @@ -21,7 +20,7 @@ export default (state, action) => { } case actionNames.VIEW_UPDATE_BASE_HOST: { - const baseHost = action.host; + const baseHost = action.payload.host; return {...state, view: {...state.view, baseHost}}; } @@ -42,10 +41,10 @@ export default (state, action) => { return {...state, view: {...state.view, viewMode: action.payload}}; case actionNames.VIEW_UPDATE_FILTER_BY_NAME: - return {...state, view: {...state.view, testNameFilter: action.testNameFilter}}; + return {...state, view: {...state.view, testNameFilter: action.payload}}; case actionNames.VIEW_SET_STRICT_MATCH_FILTER: - return {...state, view: {...state.view, strictMatchFilter: action.strictMatchFilter}}; + return {...state, view: {...state.view, strictMatchFilter: action.payload}}; case actionNames.GROUP_TESTS_BY_KEY: return {...state, view: {...state.view, keyToGroupTestsBy: action.payload}}; @@ -53,26 +52,8 @@ export default (state, action) => { case actionNames.BROWSERS_SELECTED: return {...state, view: {...state.view, filteredBrowsers: action.payload.browsers}}; - case actionNames.VIEW_THREE_UP_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.THREE_UP.id}}; - - case actionNames.VIEW_THREE_UP_SCALED_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.THREE_UP_SCALED.id}}; - - case actionNames.VIEW_THREE_UP_SCALED_TO_FIT_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.THREE_UP_SCALED_TO_FIT.id}}; - - case actionNames.VIEW_ONLY_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.ONLY_DIFF.id}}; - - case actionNames.VIEW_SWITCH_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.SWITCH.id}}; - - case actionNames.VIEW_SWIPE_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.SWIPE.id}}; - - case actionNames.VIEW_ONION_SKIN_DIFF: - return {...state, view: {...state.view, diffMode: DiffModes.ONION_SKIN.id}}; + case actionNames.SET_DIFF_MODE: + return {...state, view: {...state.view, diffMode: action.payload.diffModeId}}; default: return state; diff --git a/lib/static/modules/store.js b/lib/static/modules/store.js index f34ec85fa..f92dcbc3c 100644 --- a/lib/static/modules/store.js +++ b/lib/static/modules/store.js @@ -3,11 +3,11 @@ import {applyMiddleware, compose, createStore} from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; -import metrika from './middlewares/metrika'; -import YandexMetrika from './yandex-metrika'; +import {getMetrikaMiddleware} from './middlewares/metrika'; +import {YandexMetrika} from './yandex-metrika'; import localStorage from './middlewares/local-storage'; -const middlewares = [thunk, metrika(YandexMetrika), localStorage]; +const middlewares = [thunk, localStorage]; let composeEnhancers = compose; @@ -20,6 +20,17 @@ if (process.env.NODE_ENV !== 'production') { } } +const metrikaConfig = (window.data || {}).config?.yandexMetrika; +const areAnalyticsEnabled = metrikaConfig?.enabled && metrikaConfig?.counterId; +const isYaMetrikaAvailable = window.ym && typeof window.ym === 'function'; + +if (areAnalyticsEnabled && isYaMetrikaAvailable) { + const metrika = new YandexMetrika(); + const metrikaMiddleware = getMetrikaMiddleware(metrika); + + middlewares.push(metrikaMiddleware); +} + const createStoreWithMiddlewares = composeEnhancers(applyMiddleware(...middlewares))(createStore); export default createStoreWithMiddlewares(reducer, {}); diff --git a/lib/static/modules/utils/performance.ts b/lib/static/modules/utils/performance.ts new file mode 100644 index 000000000..8834700e0 --- /dev/null +++ b/lib/static/modules/utils/performance.ts @@ -0,0 +1,9 @@ +export function extractPerformanceMarks(): Record { + const marks = performance?.getEntriesByType?.('mark') || []; + + return marks.reduce((acc, {name, startTime}) => { + acc[name] = Math.round(startTime); + + return acc; + }, {} as Record); +} diff --git a/lib/static/modules/web-vitals.js b/lib/static/modules/web-vitals.js deleted file mode 100644 index acf27f7af..000000000 --- a/lib/static/modules/web-vitals.js +++ /dev/null @@ -1,12 +0,0 @@ -import {getCLS, getFID, getFCP, getLCP, getTTFB} from 'web-vitals'; - -export const measurePerformance = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - } -}; - diff --git a/lib/static/modules/web-vitals.ts b/lib/static/modules/web-vitals.ts new file mode 100644 index 000000000..d93ce2989 --- /dev/null +++ b/lib/static/modules/web-vitals.ts @@ -0,0 +1,9 @@ +import {getCLS, getFID, getFCP, getLCP, getTTFB, ReportHandler} from 'web-vitals'; + +export const measurePerformance = (onReport: ReportHandler): void => { + getCLS(onReport); + getFID(onReport); + getFCP(onReport); + getLCP(onReport); + getTTFB(onReport); +}; diff --git a/lib/static/modules/yandex-metrika.js b/lib/static/modules/yandex-metrika.js deleted file mode 100644 index 3c9f645b3..000000000 --- a/lib/static/modules/yandex-metrika.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -import {isNumber, isFunction} from 'lodash'; - -const REACH_GOAL = 'reachGoal'; -const PARAMS = 'params'; -const targets = { - ACCEPT_SCREENSHOT: 'ACCEPT_SCREENSHOT', - ACCEPT_OPENED_SCREENSHOTS: 'ACCEPT_OPENED_SCREENSHOTS' -}; - -class YandexMetrika { - static create(config) { - return new this(config); - } - - constructor(config) { - this._config = config; - } - - _isEnabled() { - return isNumber(this._config.counterNumber) && window.ym && isFunction(window.ym); - } - - _register(cb) { - if (!this._isEnabled()) { - return; - } - - cb(); - } - - _registerGoal(target, goalParams = {}) { - this._register(() => window.ym(this._config.counterNumber, REACH_GOAL, target, goalParams)); - } - - sendVisitParams(params) { - this._register(() => { - window.ym(this._config.counterNumber, PARAMS, params); - }); - } - - acceptScreenshot(params = {acceptedImagesCount: 1}) { - this._registerGoal(targets.ACCEPT_SCREENSHOT, params); - } - - acceptOpenedScreenshots(params) { - this._registerGoal(targets.ACCEPT_OPENED_SCREENSHOTS, params); - } -} - -export default YandexMetrika; diff --git a/lib/static/modules/yandex-metrika.ts b/lib/static/modules/yandex-metrika.ts new file mode 100644 index 000000000..fde9f0cb7 --- /dev/null +++ b/lib/static/modules/yandex-metrika.ts @@ -0,0 +1,67 @@ +enum YandexMetrikaMethod { + ReachGoal = 'reachGoal', + Params = 'params', +} + +interface YandexMetrikaSdk { + (counterId: number, method: YandexMetrikaMethod.ReachGoal, goalId: string, goalParams: Record): void; + (counterId: number, method: YandexMetrikaMethod.Params, params: Record): void; +} + +declare global { + interface Window { + ym?: YandexMetrikaSdk; + } +} + +enum GoalId { + ScreenshotAccept = 'ACCEPT_SCREENSHOT', + OpenedScreenshotsAccept = 'ACCEPT_OPENED_SCREENSHOTS', + FeatureUsage = 'FEATURE_USAGE' +} + +interface ScreenshotAcceptData { + acceptedImagesCount: number; +} + +interface FeatureUsageData { + featureName: string; +} + +export class YandexMetrika { + protected readonly _isEnabled: boolean; + protected readonly _counterNumber: number; + + constructor(isEnabled: boolean, counterNumber: number) { + this._isEnabled = isEnabled; + this._counterNumber = counterNumber; + } + + protected _registerGoal(goalId: GoalId, goalParams = {}): void { + if (!this._isEnabled) { + return; + } + + window.ym?.(this._counterNumber, YandexMetrikaMethod.ReachGoal, goalId, goalParams); + } + + setVisitParams(params: Record): void { + if (!this._isEnabled) { + return; + } + + window.ym?.(this._counterNumber, YandexMetrikaMethod.Params, params); + } + + trackScreenshotsAccept(params: ScreenshotAcceptData = {acceptedImagesCount: 1}): void { + this._registerGoal(GoalId.ScreenshotAccept, params); + } + + trackOpenedScreenshotsAccept(params: ScreenshotAcceptData): void { + this._registerGoal(GoalId.OpenedScreenshotsAccept, params); + } + + trackFeatureUsage(params: FeatureUsageData): void { + this._registerGoal(GoalId.FeatureUsage, params); + } +} diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx index c88f5cfd7..6c44b9fe1 100644 --- a/lib/static/new-ui/app/App.tsx +++ b/lib/static/new-ui/app/App.tsx @@ -13,6 +13,9 @@ import {SuitesPage} from '../features/suites/components/SuitesPage'; import {VisualChecksPage} from '../features/visual-checks/components/VisualChecksPage'; import '../../new-ui.css'; import store from '../../modules/store'; +import {CustomScripts} from '@/static/new-ui/components/CustomScripts'; +import {State} from '@/static/new-ui/types/store'; +import {AnalyticsProvider} from '@/static/new-ui/providers/analytics'; export function App(): ReactNode { const pages = [ @@ -26,21 +29,26 @@ export function App(): ReactNode { {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: } ]; + const customScripts = (store.getState() as State).config.customScripts; + return + - - - - - } path={'/'}/> - {pages.map(page => {page.children})} - - - - - + + + + + + } path={'/'}/> + {pages.map(page => {page.children})} + + + + + + diff --git a/lib/static/new-ui/app/gui.tsx b/lib/static/new-ui/app/gui.tsx index 4583e9781..efe7f6fd4 100644 --- a/lib/static/new-ui/app/gui.tsx +++ b/lib/static/new-ui/app/gui.tsx @@ -4,7 +4,14 @@ import {createRoot} from 'react-dom/client'; import {ClientEvents} from '@/gui/constants'; import {App} from './App'; import store from '../../modules/store'; -import {finGuiReport, thunkInitGuiReport, suiteBegin, testBegin, testResult, testsEnd} from '../../modules/actions'; +import { + finGuiReport, + thunkInitGuiReport, + suiteBegin, + testBegin, + testResult, + thunkTestsEnd +} from '../../modules/actions'; import {setGuiServerConnectionStatus} from '@/static/modules/actions/gui-server-connection'; import actionNames from '@/static/modules/action-names'; @@ -47,7 +54,7 @@ function Gui(): ReactNode { }); eventSource.addEventListener(ClientEvents.END, () => { - store.dispatch(testsEnd()); + store.dispatch(thunkTestsEnd()); }); }; diff --git a/lib/static/new-ui/components/AttemptPicker/index.tsx b/lib/static/new-ui/components/AttemptPicker/index.tsx index 96aeb10c1..e7706b98c 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.tsx +++ b/lib/static/new-ui/components/AttemptPicker/index.tsx @@ -7,7 +7,7 @@ import styles from './index.module.css'; import classNames from 'classnames'; import {Button, Icon, Spin} from '@gravity-ui/uikit'; import {RunTestsFeature} from '@/constants'; -import {retryTest} from '@/static/modules/actions'; +import {thunkRunTest} from '@/static/modules/actions'; import {getCurrentBrowser, getCurrentResultId} from '@/static/new-ui/features/suites/selectors'; interface AttemptPickerProps { @@ -39,7 +39,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { const onRetryTestHandler = (): void => { if (currentBrowser) { - dispatch(retryTest({testName: currentBrowser.parentId, browserName: currentBrowser.name})); + dispatch(thunkRunTest({test: {testName: currentBrowser.parentId, browserName: currentBrowser.name}})); } }; diff --git a/lib/static/components/custom-scripts.jsx b/lib/static/new-ui/components/CustomScripts/index.tsx similarity index 50% rename from lib/static/components/custom-scripts.jsx rename to lib/static/new-ui/components/CustomScripts/index.tsx index 74b2be878..3680ad38d 100644 --- a/lib/static/components/custom-scripts.jsx +++ b/lib/static/new-ui/components/CustomScripts/index.tsx @@ -1,30 +1,26 @@ -import React, {useEffect, useRef} from 'react'; -import PropTypes from 'prop-types'; -import {isEmpty} from 'lodash'; +import React, {ReactNode, useEffect, useRef} from 'react'; -function CustomScripts(props) { +interface CustomScriptProps { + scripts: (string | ((...args: never) => unknown))[]; +} + +export function CustomScripts(props: CustomScriptProps): ReactNode { const {scripts} = props; - if (isEmpty(scripts)) { + if (scripts.length === 0) { return null; } - const ref = useRef(null); + const ref = useRef(null); useEffect(() => { scripts.forEach((script) => { const s = document.createElement('script'); s.type = 'text/javascript'; s.innerHTML = `(${script})();`; - ref.current.appendChild(s); + ref.current?.appendChild(s); }); }, [scripts]); return
; } - -CustomScripts.propTypes = { - scripts: PropTypes.array.isRequired -}; - -export default CustomScripts; diff --git a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx index 397d7f1f0..bb4c0ac92 100644 --- a/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx +++ b/lib/static/new-ui/features/suites/components/ScreenshotsTreeViewItem/index.tsx @@ -7,16 +7,16 @@ import {AssertViewResult} from '@/static/new-ui/components/AssertViewResult'; import {ImageEntity} from '@/static/new-ui/types/store'; import {DiffModeId, DiffModes, EditScreensFeature, TestStatus} from '@/constants'; import { - acceptTest, - changeDiffMode, + setDiffMode, staticAccepterStageScreenshot, - staticAccepterUnstageScreenshot, - undoAcceptImage + staticAccepterUnstageScreenshot } from '@/static/modules/actions'; import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; import {getCurrentBrowser, getCurrentResult} from '@/static/new-ui/features/suites/selectors'; import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; import styles from './index.module.css'; +import {thunkAcceptImages, thunkRevertImages} from '@/static/modules/actions/screenshots'; +import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics'; interface ScreenshotsTreeViewItemProps { image: ImageEntity; @@ -25,6 +25,8 @@ interface ScreenshotsTreeViewItemProps { export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): ReactNode { const dispatch = useDispatch(); + const analytics = useAnalytics(); + const diffMode = useSelector(state => state.view.diffMode); const isEditScreensAvailable = useSelector(state => state.app.availableFeatures) .find(feature => feature.name === EditScreensFeature.name); @@ -40,22 +42,24 @@ export function ScreenshotsTreeViewItem(props: ScreenshotsTreeViewItemProps): Re const isLastResult = currentResult && currentBrowser && currentResult.id === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]; const isUndoAvailable = isScreenRevertable({gui: isGui, image: props.image, isLastResult, isStaticImageAccepterEnabled}); - const onDiffModeChangeHandler = (diffMode: DiffModeId): void => { - dispatch(changeDiffMode(diffMode)); + const onDiffModeChangeHandler = (diffModeId: DiffModeId): void => { + dispatch(setDiffMode({diffModeId})); }; const onScreenshotAccept = (): void => { + analytics?.trackScreenshotsAccept(); + if (isStaticImageAccepterEnabled) { dispatch(staticAccepterStageScreenshot([props.image.id])); } else { - dispatch(acceptTest(props.image.id)); + dispatch(thunkAcceptImages({imageIds: [props.image.id]})); } }; const onScreenshotUndo = (): void => { if (isStaticImageAccepterEnabled) { dispatch(staticAccepterUnstageScreenshot([props.image.id])); } else { - dispatch(undoAcceptImage(props.image.id)); + dispatch(thunkRevertImages({imageIds: [props.image.id]})); } }; diff --git a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx index f5fb7bdf9..fdab07327 100644 --- a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx @@ -30,7 +30,7 @@ interface TestStatusFilterProps { function TestStatusFilterInternal({statusCounts, actions, viewMode}: TestStatusFilterProps): ReactNode { const isInitialized = useSelector(getIsInitialized); - return void actions.changeViewMode(e.target.value)} value={viewMode}> + return void actions.changeViewMode(e.target.value as ViewMode)} value={viewMode}> } /> } /> } /> diff --git a/lib/static/new-ui/features/suites/components/TreeActionsToolbar/index.tsx b/lib/static/new-ui/features/suites/components/TreeActionsToolbar/index.tsx index 08957597f..96d37213b 100644 --- a/lib/static/new-ui/features/suites/components/TreeActionsToolbar/index.tsx +++ b/lib/static/new-ui/features/suites/components/TreeActionsToolbar/index.tsx @@ -18,14 +18,11 @@ import {useDispatch, useSelector} from 'react-redux'; import styles from './index.module.css'; import { - acceptOpened, deselectAll, - retrySuite, selectAll, setAllTreeNodesState, setTreeViewMode, staticAccepterStageScreenshot, - staticAccepterUnstageScreenshot, - undoAcceptImages + staticAccepterUnstageScreenshot, thunkRunTests } from '@/static/modules/actions'; import {ImageEntity, TreeViewMode} from '@/static/new-ui/types/store'; import {CHECKED, INDETERMINATE} from '@/constants/checked-statuses'; @@ -47,6 +44,8 @@ import {EditScreensFeature, RunTestsFeature} from '@/constants'; import {getTreeViewItems} from '@/static/new-ui/features/suites/components/SuitesTreeView/selectors'; import {GroupBySelect} from '@/static/new-ui/features/suites/components/GroupBySelect'; import {SortBySelect} from '@/static/new-ui/features/suites/components/SortBySelect'; +import {thunkAcceptImages, thunkRevertImages} from '@/static/modules/actions/screenshots'; +import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics'; interface TreeActionsToolbarProps { onHighlightCurrentTest?: () => void; @@ -54,6 +53,7 @@ interface TreeActionsToolbarProps { export function TreeActionsToolbar(props: TreeActionsToolbarProps): ReactNode { const dispatch = useDispatch(); + const analytics = useAnalytics(); const rootSuiteIds = useSelector(state => state.tree.suites.allRootIds); const suitesStateById = useSelector(state => state.tree.suites.stateById); @@ -123,13 +123,13 @@ export function TreeActionsToolbar(props: TreeActionsToolbarProps): ReactNode { const handleRun = (): void => { if (isSelectedAtLeastOne) { - dispatch(retrySuite(selectedTests)); + dispatch(thunkRunTests({tests: selectedTests})); } else { const visibleTests = visibleBrowserIds.map(browserId => ({ testName: browsersById[browserId].parentId, browserName: browsersById[browserId].name })); - dispatch(retrySuite(visibleTests)); + dispatch(thunkRunTests({tests: visibleTests})); } }; @@ -141,7 +141,7 @@ export function TreeActionsToolbar(props: TreeActionsToolbarProps): ReactNode { if (isStaticImageAccepterEnabled) { dispatch(staticAccepterUnstageScreenshot(acceptableImageIds)); } else { - dispatch(undoAcceptImages(acceptableImageIds)); + dispatch(thunkRevertImages({imageIds: acceptableImageIds})); } }; @@ -149,11 +149,12 @@ export function TreeActionsToolbar(props: TreeActionsToolbarProps): ReactNode { const acceptableImageIds = activeImages .filter(image => isAcceptable(image)) .map(image => image.id); + analytics?.trackScreenshotsAccept({acceptedImagesCount: acceptableImageIds.length}); if (isStaticImageAccepterEnabled) { dispatch(staticAccepterStageScreenshot(acceptableImageIds)); } else { - dispatch(acceptOpened(acceptableImageIds)); + dispatch(thunkAcceptImages({imageIds: acceptableImageIds})); } }; diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx index 35efb0bfd..3cbf830f8 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx @@ -17,9 +17,8 @@ import styles from './index.module.css'; import {CompactAttemptPicker} from '@/static/new-ui/components/CompactAttemptPicker'; import {DiffModeId, DiffModes, EditScreensFeature} from '@/constants'; import { - acceptTest, - changeDiffMode, staticAccepterStageScreenshot, staticAccepterUnstageScreenshot, - undoAcceptImage, + setDiffMode, + staticAccepterStageScreenshot, staticAccepterUnstageScreenshot, visualChecksPageSetCurrentNamedImage } from '@/static/modules/actions'; import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; @@ -27,9 +26,12 @@ import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; import { AssertViewResultSkeleton } from '@/static/new-ui/features/visual-checks/components/VisualChecksPage/AssertViewResultSkeleton'; +import {thunkAcceptImages, thunkRevertImages} from '@/static/modules/actions/screenshots'; +import {useAnalytics} from '@/static/new-ui/hooks/useAnalytics'; export function VisualChecksPage(): ReactNode { const dispatch = useDispatch(); + const analytics = useAnalytics(); const currentNamedImage = useSelector(getCurrentNamedImage); const currentImage = useSelector(getCurrentImage); @@ -40,8 +42,8 @@ export function VisualChecksPage(): ReactNode { const onNextImageHandler = (): void => void dispatch(visualChecksPageSetCurrentNamedImage(visibleNamedImageIds[currentNamedImageIndex + 1])); const diffMode = useSelector(state => state.view.diffMode); - const onChangeHandler = (diffMode: DiffModeId): void => { - dispatch(changeDiffMode(diffMode)); + const onChangeHandler = (diffModeId: DiffModeId): void => { + dispatch(setDiffMode({diffModeId})); }; const isStaticImageAccepterEnabled = useSelector(state => state.staticImageAccepter.enabled); @@ -55,11 +57,12 @@ export function VisualChecksPage(): ReactNode { if (!currentImage) { return; } + analytics?.trackScreenshotsAccept(); if (isStaticImageAccepterEnabled) { dispatch(staticAccepterStageScreenshot([currentImage.id])); } else { - dispatch(acceptTest(currentImage.id)); + dispatch(thunkAcceptImages({imageIds: [currentImage.id]})); } }; const onScreenshotUndo = (): void => { @@ -70,7 +73,7 @@ export function VisualChecksPage(): ReactNode { if (isStaticImageAccepterEnabled) { dispatch(staticAccepterUnstageScreenshot([currentImage.id])); } else { - dispatch(undoAcceptImage(currentImage.id)); + dispatch(thunkRevertImages({imageIds: [currentImage.id]})); } }; diff --git a/lib/static/new-ui/hooks/useAnalytics.ts b/lib/static/new-ui/hooks/useAnalytics.ts new file mode 100644 index 000000000..3abd1fb05 --- /dev/null +++ b/lib/static/new-ui/hooks/useAnalytics.ts @@ -0,0 +1,15 @@ +import {useContext} from 'react'; + +import {AnalyticsContext} from '@/static/new-ui/providers/analytics'; +import {YandexMetrika} from '@/static/modules/yandex-metrika'; +import {NEW_ISSUE_LINK} from '@/constants'; + +export const useAnalytics = (): YandexMetrika | null => { + const analytics = useContext(AnalyticsContext); + + if (!analytics) { + console.warn('Failed to get analytics class instance to send usage info. If you are a user, you can safely ignore this. Feel free to report it to use at ' + NEW_ISSUE_LINK); + } + + return analytics; +}; diff --git a/lib/static/new-ui/providers/analytics.tsx b/lib/static/new-ui/providers/analytics.tsx new file mode 100644 index 000000000..e871fe557 --- /dev/null +++ b/lib/static/new-ui/providers/analytics.tsx @@ -0,0 +1,19 @@ +import React, {createContext, ReactNode, useMemo} from 'react'; +import {YandexMetrika} from '@/static/modules/yandex-metrika'; +import {getAreAnalyticsEnabled, getCounterId} from '@/static/new-ui/utils/analytics'; + +export const AnalyticsContext = createContext(null); + +interface AnalyticsProviderProps { + children: React.ReactNode; +} + +export const AnalyticsProvider = ({children}: AnalyticsProviderProps): ReactNode => { + const areAnalyticsEnabled = getAreAnalyticsEnabled(); + const counterId = getCounterId(); + const analytics = useMemo(() => new YandexMetrika(areAnalyticsEnabled, counterId), []); + + return + {children} + ; +}; diff --git a/lib/static/new-ui/utils/analytics.ts b/lib/static/new-ui/utils/analytics.ts new file mode 100644 index 000000000..8e11dfd83 --- /dev/null +++ b/lib/static/new-ui/utils/analytics.ts @@ -0,0 +1,21 @@ +import {DataForStaticFile} from '@/server-utils'; + +declare global { + interface Window { + data?: DataForStaticFile + } +} + +export const getAreAnalyticsEnabled = (): boolean => { + const metrikaConfig = (window.data || {}).config?.yandexMetrika; + const areAnalyticsEnabled = metrikaConfig?.enabled && metrikaConfig?.counterNumber; + const isYaMetrikaAvailable = window.ym && typeof window.ym === 'function'; + + return Boolean(areAnalyticsEnabled && isYaMetrikaAvailable); +}; + +export const getCounterId = (): number => { + const metrikaConfig = (window.data || {}).config?.yandexMetrika; + + return metrikaConfig?.counterNumber ?? 0; +}; diff --git a/lib/tests-tree-builder/base.ts b/lib/tests-tree-builder/base.ts index cced256f0..1c4382ca4 100644 --- a/lib/tests-tree-builder/base.ts +++ b/lib/tests-tree-builder/base.ts @@ -44,9 +44,9 @@ export type TreeImage = { export interface Tree { suites: { - byId: Record, - allIds: string[], - allRootIds: string[] + byId: Record; + allIds: string[]; + allRootIds: string[]; }, browsers: { byId: Record, diff --git a/lib/types.ts b/lib/types.ts index ad5ee948c..93d8282bf 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -244,7 +244,7 @@ export interface ReporterConfig { pluginsEnabled: boolean; saveErrorDetails: boolean; saveFormat: SaveFormat; - yandexMetrika: { counterNumber: null | number }; + yandexMetrika: { enabled?: boolean; counterNumber: null | number }; staticImageAccepter: StaticImageAccepterConfig; } diff --git a/test/unit/lib/static/components/controls/accept-opened-button.jsx b/test/unit/lib/static/components/controls/accept-opened-button.jsx index bc4adb98e..78809983a 100644 --- a/test/unit/lib/static/components/controls/accept-opened-button.jsx +++ b/test/unit/lib/static/components/controls/accept-opened-button.jsx @@ -11,7 +11,7 @@ describe('', () => { let selectors; beforeEach(() => { - actionsStub = {acceptOpened: sandbox.stub().returns({type: 'some-type'})}; + actionsStub = {thunkAcceptImages: sandbox.stub().returns({type: 'some-type'})}; selectors = {getAcceptableOpenedImageIds: sandbox.stub().returns([])}; AcceptOpenedButton = proxyquire('lib/static/components/controls/accept-opened-button', { @@ -52,7 +52,7 @@ describe('', () => { assert.isFalse(component.getByRole('button').disabled); }); - it('should call "acceptOpened" action on click', async () => { + it('should call "thunkAcceptImages" action on click', async () => { const user = userEvent.setup(); const state = mkState({initialState: {processing: false}}); const acceptableOpenedImageIds = ['img-id-1']; @@ -61,6 +61,6 @@ describe('', () => { const component = mkConnectedComponent(, state); await user.click(component.getByRole('button')); - assert.calledOnceWith(actionsStub.acceptOpened, acceptableOpenedImageIds); + assert.calledOnceWith(actionsStub.thunkAcceptImages, {imageIds: acceptableOpenedImageIds}); }); }); diff --git a/test/unit/lib/static/components/controls/custom-gui-controls.jsx b/test/unit/lib/static/components/controls/custom-gui-controls.jsx index 3845044fe..108aa8e5e 100644 --- a/test/unit/lib/static/components/controls/custom-gui-controls.jsx +++ b/test/unit/lib/static/components/controls/custom-gui-controls.jsx @@ -14,7 +14,7 @@ describe('', () => { beforeEach(() => { actionsStub = { - runCustomGuiAction: sandbox.stub().returns({type: 'some-type'}) + thunkRunCustomGuiAction: sandbox.stub().returns({type: 'some-type'}) }; CustomGuiControls = proxyquire('lib/static/components/controls/custom-gui-controls', { @@ -117,7 +117,7 @@ describe('', () => { await user.click(component.getByRole('button')); - assert.calledOnceWith(actionsStub.runCustomGuiAction); + assert.calledOnceWith(actionsStub.thunkRunCustomGuiAction); }); it(`should set active ${controlType}`, () => { diff --git a/test/unit/lib/static/components/controls/find-same-diffs-button.jsx b/test/unit/lib/static/components/controls/find-same-diffs-button.jsx index ad906a782..482fe3160 100644 --- a/test/unit/lib/static/components/controls/find-same-diffs-button.jsx +++ b/test/unit/lib/static/components/controls/find-same-diffs-button.jsx @@ -34,7 +34,7 @@ describe('', () => { }; beforeEach(() => { - actionsStub = {findSameDiffs: sandbox.stub().returns({type: 'some-type'})}; + actionsStub = {thunkFindSameDiffs: sandbox.stub().returns({type: 'some-type'})}; selectors = {getFailedOpenedImageIds: sandbox.stub().returns([])}; FindSameDiffsButton = proxyquire('lib/static/components/controls/find-same-diffs-button', { @@ -80,6 +80,6 @@ describe('', () => { ); await user.click(component.getByTestId('find-same-diffs')); - assert.calledOnceWith(actionsStub.findSameDiffs, 'img-1', failedOpenedImageIds, 'yabro'); + assert.calledOnceWith(actionsStub.thunkFindSameDiffs, 'img-1', failedOpenedImageIds, 'yabro'); }); }); diff --git a/test/unit/lib/static/components/controls/gui-controls.jsx b/test/unit/lib/static/components/controls/gui-controls.jsx index af389f0de..37d6f75b1 100644 --- a/test/unit/lib/static/components/controls/gui-controls.jsx +++ b/test/unit/lib/static/components/controls/gui-controls.jsx @@ -16,7 +16,7 @@ describe('', () => { actionsStub = { runAllTests: sandbox.stub().returns({type: 'some-type'}), runFailedTests: sandbox.stub().returns({type: 'some-type'}), - stopTests: sandbox.stub().returns({type: 'some-type'}) + thunkStopTests: sandbox.stub().returns({type: 'some-type'}) }; selectors = { getFailedTests: sandbox.stub().returns([]), @@ -82,7 +82,7 @@ describe('', () => { await user.click(stop); - assert.calledOnce(actionsStub.stopTests); + assert.calledOnce(actionsStub.thunkStopTests); }); }); }); diff --git a/test/unit/lib/static/components/controls/run-button.jsx b/test/unit/lib/static/components/controls/run-button.jsx index a22747a7e..a89d2bf0d 100644 --- a/test/unit/lib/static/components/controls/run-button.jsx +++ b/test/unit/lib/static/components/controls/run-button.jsx @@ -13,9 +13,9 @@ describe('', () => { useLocalStorageStub = sandbox.stub().returns([true]); useLocalStorageStub.withArgs('RunMode', 'Failed').returns(['All', writeValueStub]); actionsStub = { - runAllTests: sandbox.stub().returns({type: 'some-type'}), - runFailedTests: sandbox.stub().returns({type: 'some-type'}), - retrySuite: sandbox.stub().returns({type: 'some-type'}) + thunkRunAllTests: sandbox.stub().returns({type: 'some-type'}), + thunkRunFailedTests: sandbox.stub().returns({type: 'some-type'}), + thunkRunSuite: sandbox.stub().returns({type: 'some-type'}) }; selectorsStub = { getFailedTests: sandbox.stub().returns([]), @@ -70,10 +70,10 @@ describe('', () => { initialState: {autoRun: true} }); - assert.calledOnce(actionsStub.runAllTests); + assert.calledOnce(actionsStub.thunkRunAllTests); }); - it('should call "runAllTests" action on "Run all tests" click', async () => { + it('should call "thunkRunAllTests" action on "Run all tests" click', async () => { const user = userEvent.setup(); const component = mkConnectedComponent(, { initialState: {tree: {suites: {allRootIds: ['suite']}}, processing: false} @@ -81,7 +81,7 @@ describe('', () => { await user.click(component.getByRole('button')); - assert.calledOnce(actionsStub.runAllTests); + assert.calledOnce(actionsStub.thunkRunAllTests); }); it('should call "runFailedTests" action on "Run failed tests" click', async () => { @@ -96,7 +96,7 @@ describe('', () => { await user.click(component.getByText('Failed Tests', {selector: '[role=combobox] > *'})); await user.click(component.getByRole('button')); - assert.calledOnceWith(actionsStub.runFailedTests, failedTests); + assert.calledOnceWith(actionsStub.thunkRunFailedTests, {tests: failedTests}); }); it('should call "retrySuite" action on "Run checked tests" click', async () => { @@ -111,7 +111,7 @@ describe('', () => { await user.click(component.getByText('Checked Tests', {selector: '[role=combobox] > *'})); await user.click(component.getByRole('button')); - assert.calledOnceWith(actionsStub.retrySuite, checkedTests); + assert.calledOnceWith(actionsStub.thunkRunSuite, {tests: checkedTests}); }); describe('Label', () => { diff --git a/test/unit/lib/static/components/custom-scripts.tsx b/test/unit/lib/static/components/custom-scripts.tsx index 31629a16c..04fa73407 100644 --- a/test/unit/lib/static/components/custom-scripts.tsx +++ b/test/unit/lib/static/components/custom-scripts.tsx @@ -1,47 +1,37 @@ import {expect} from 'chai'; import React from 'react'; -import CustomScripts from 'lib/static/components/custom-scripts'; +import {CustomScripts} from '@/static/new-ui/components/CustomScripts'; import {render} from '@testing-library/react'; describe('', () => { it('should render component for scripts', () => { - const props = { - scripts: [function(): void {}] - }; + const scripts = [function(): void {}]; - const component = render(); + const component = render(); expect(component.container.querySelector('.custom-scripts')).to.exist; }); it('should not render component if no scripts to inject', () => { - const props = { - scripts: [] - }; - - const component = render(); + const component = render(); expect(component.container.querySelector('.custom-scripts')).to.not.exist; }); it('should wrap function with IIFE', () => { - const props = { - scripts: [function foo(): void {}] - }; + const scripts = [function foo(): void {}]; - const component = render(); + const component = render(); const script = component.container.textContent; assert.equal(script, '(function foo() {})();'); }); it('should split each function with ";"', () => { - const props = { - scripts: [ - function foo(): void {}, - function bar(): void {} - ] - }; - - const component = render(); + const scripts = [ + function foo(): void {}, + function bar(): void {} + ]; + + const component = render(); const script = component.container.textContent; assert.equal(script, '(function foo() {})();(function bar() {})();'); diff --git a/test/unit/lib/static/components/modals/screenshot-accepter/index.jsx b/test/unit/lib/static/components/modals/screenshot-accepter/index.jsx index a6156d44a..22bb0cf96 100644 --- a/test/unit/lib/static/components/modals/screenshot-accepter/index.jsx +++ b/test/unit/lib/static/components/modals/screenshot-accepter/index.jsx @@ -248,7 +248,7 @@ describe('', () => { await user.click(component.getByText('Accept', {selector: 'button > *'})); await user.click(component.getByTitle('Close mode with fast screenshot accepting', {exact: false})); - assert.calledWith(reduxAction, sinon.match({type: 'APPLY_DELAYED_TEST_RESULTS'})); + assert.calledWith(reduxAction, sinon.match({type: 'COMMIT_ACCEPTED_IMAGES_TO_TREE'})); }); it('should not apply delayed test result, if it is cancelled by "Undo"', async () => { diff --git a/test/unit/lib/static/components/section/body/index.jsx b/test/unit/lib/static/components/section/body/index.jsx index 61afb2f17..6a123cb31 100644 --- a/test/unit/lib/static/components/section/body/index.jsx +++ b/test/unit/lib/static/components/section/body/index.jsx @@ -24,7 +24,7 @@ describe('', () => { beforeEach(() => { actionsStub = { - retryTest: sandbox.stub().returns({type: 'some-type'}), + thunkRunTest: sandbox.stub().returns({type: 'some-type'}), changeTestRetry: sandbox.stub().returns({type: 'some-type'}) }; @@ -74,7 +74,7 @@ describe('', () => { assert.isFalse(component.getByTestId('test-retry').disabled); }); - it('should call action "retryTest" on "handler" prop calling', async () => { + it('should call action "thunkRunTest" on "handler" prop calling', async () => { const user = userEvent.setup(); const testName = 'suite test'; const browserName = 'yabro'; @@ -82,7 +82,7 @@ describe('', () => { await user.click(component.getByTestId('test-retry')); - assert.calledOnceWith(actionsStub.retryTest, {testName, browserName}); + assert.calledOnceWith(actionsStub.thunkRunTest, {test: {testName, browserName}}); }); }); diff --git a/test/unit/lib/static/components/state/index.jsx b/test/unit/lib/static/components/state/index.jsx index c055391b4..8af51d62f 100644 --- a/test/unit/lib/static/components/state/index.jsx +++ b/test/unit/lib/static/components/state/index.jsx @@ -35,8 +35,8 @@ describe('', () => { actionsStub = { toggleStateResult: sandbox.stub().returns({type: 'some-type'}), - acceptTest: sandbox.stub().returns({type: 'some-type'}), - undoAcceptImage: sandbox.stub().returns({type: 'some-type'}), + thunkAcceptImages: sandbox.stub().returns({type: 'some-type'}), + thunkRevertImages: sandbox.stub().returns({type: 'some-type'}), openModal: sandbox.stub().returns({type: 'some-type'}) }; @@ -121,7 +121,7 @@ describe('', () => { assert.isFalse(stateComponent.queryByTestId('test-accept').disabled); }); - it('should call "acceptTest" action on button click', async () => { + it('should call "thunkAcceptImages" action on button click', async () => { const user = userEvent.setup(); const image = {stateName: 'some-name', status: FAIL}; const initialState = { @@ -137,7 +137,7 @@ describe('', () => { const stateComponent = mkStateComponent({imageId: 'img-id'}, initialState); await user.click(stateComponent.queryByTestId('test-accept')); - assert.calledOnceWith(actionsStub.acceptTest, 'img-id'); + assert.calledOnceWith(actionsStub.thunkAcceptImages, {imageIds: ['img-id']}); }); }); @@ -190,7 +190,7 @@ describe('', () => { await user.click(stateComponent.queryByTestId('test-undo')); - assert.calledOnceWith(actionsStub.undoAcceptImage, 'img-id'); + assert.calledOnceWith(actionsStub.thunkRevertImages, {imageIds: ['img-id']}); }); }); diff --git a/test/unit/lib/static/modules/actions/custom-gui.ts b/test/unit/lib/static/modules/actions/custom-gui.ts new file mode 100644 index 000000000..29ca18d27 --- /dev/null +++ b/test/unit/lib/static/modules/actions/custom-gui.ts @@ -0,0 +1,92 @@ +import * as customGuiActions from '@/static/modules/actions/custom-gui'; +import axiosOriginal from 'axios'; +import actionNames from '@/static/modules/action-names'; +import sinon, {SinonStub, SinonStubbedInstance} from 'sinon'; +import proxyquire from 'proxyquire'; + +const axios = axiosOriginal as unknown as SinonStubbedInstance; + +describe('lib/static/modules/actions/custom-gui', () => { + const sandbox = sinon.sandbox.create(); + let dispatch: SinonStub; + let createNotificationError: SinonStub; + let actions: typeof customGuiActions; + + beforeEach(() => { + dispatch = sinon.stub(); + createNotificationError = sinon.stub(); + + sandbox.stub(axios, 'post').resolves({data: {}}); + + actions = proxyquire('lib/static/modules/actions/custom-gui', { + '@/static/modules/actions/notifications': {createNotificationError} + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('thunkRunCustomGuiAction', () => { + it('should run custom action on server for control of given group of section', async () => { + const payload = { + sectionName: 'foo', + groupIndex: 100, + controlIndex: 500 + }; + + await actions.thunkRunCustomGuiAction(payload)(dispatch, sinon.stub(), null); + + assert.calledOnceWith( + axios.post, + sinon.match.any, + sinon.match(({sectionName, groupIndex, controlIndex}: {sectionName: string; groupIndex: number; controlIndex: number}) => { + assert.equal(sectionName, 'foo'); + assert.equal(groupIndex, 100); + assert.equal(controlIndex, 500); + return true; + }) + ); + }); + + it('should dispatch action for control of given group of section', async () => { + const payload = { + sectionName: 'foo', + groupIndex: 100, + controlIndex: 500 + }; + + await actions.thunkRunCustomGuiAction(payload)(dispatch, sinon.stub(), null); + + assert.calledOnceWith( + dispatch, + { + type: actionNames.RUN_CUSTOM_GUI_ACTION, + payload: { + sectionName: 'foo', + groupIndex: 100, + controlIndex: 500 + } + } + ); + }); + + it('should show notification if error in action on the server is happened', async () => { + const payload = { + sectionName: 'foo', + groupIndex: 100, + controlIndex: 500 + }; + const customGuiError = new Error('failed to run custom gui control action'); + axios.post.throws(customGuiError); + + await actions.thunkRunCustomGuiAction(payload)(dispatch, sinon.stub(), null); + + assert.calledOnceWith( + createNotificationError, + 'runCustomGuiAction', + customGuiError + ); + }); + }); +}); diff --git a/test/unit/lib/static/modules/actions/index.js b/test/unit/lib/static/modules/actions/index.js index 5e06f3c2d..a281fa281 100644 --- a/test/unit/lib/static/modules/actions/index.js +++ b/test/unit/lib/static/modules/actions/index.js @@ -1,27 +1,21 @@ import axios from 'axios'; import proxyquire from 'proxyquire'; -import {POSITIONS} from 'reapop'; -import {acceptOpened, undoAcceptImages, retryTest, runFailedTests} from 'lib/static/modules/actions'; +import { + thunkAcceptImages, + thunkRevertImages +} from 'lib/static/modules/actions'; import actionNames from 'lib/static/modules/action-names'; -import {DiffModes} from 'lib/constants/diff-modes'; describe('lib/static/modules/actions', () => { const sandbox = sinon.sandbox.create(); - let dispatch, actions, notify, getSuitesTableRows, getMainDatabaseUrl, connectToDatabaseStub, pluginsStub; + let dispatch, actions, pluginsStub; beforeEach(() => { dispatch = sandbox.stub(); sandbox.stub(axios, 'post').resolves({data: {}}); - notify = sandbox.stub(); - getSuitesTableRows = sandbox.stub(); - getMainDatabaseUrl = sandbox.stub().returns({href: 'http://localhost/default/sqlite.db'}); - connectToDatabaseStub = sandbox.stub().resolves({}); pluginsStub = {loadAll: sandbox.stub()}; actions = proxyquire('lib/static/modules/actions', { - 'reapop': {notify}, - '../database-utils': {getSuitesTableRows}, - '../../../db-utils/client': {getMainDatabaseUrl, connectToDatabase: connectToDatabaseStub}, '../plugins': pluginsStub }); }); @@ -30,142 +24,34 @@ describe('lib/static/modules/actions', () => { sandbox.restore(); }); - describe('acceptOpened', () => { + describe('thunkAcceptImages', () => { it('should update opened images', async () => { const imageIds = ['img-id-1', 'img-id-2']; const images = [{id: 'img-id-1'}, {id: 'img-id-2'}]; axios.post.withArgs('/reference-data-to-update', imageIds).returns({data: images}); - await acceptOpened(imageIds)(dispatch); + await thunkAcceptImages({imageIds})(dispatch); assert.calledWith(axios.post.firstCall, '/reference-data-to-update', imageIds); assert.calledWith(axios.post.secondCall, '/update-reference', images); }); }); - describe('undoAcceptImages', () => { + describe('thunkRevertImages', () => { it('should cancel update of accepted image', async () => { const imageIds = ['img-id-1', 'img-id-2']; const images = [{id: 'img-id-1'}, {id: 'img-id-2'}]; axios.post.withArgs('/reference-data-to-update', imageIds).returns({data: images}); - await undoAcceptImages(imageIds)(dispatch); + await thunkRevertImages({imageIds})(dispatch); assert.calledWith(axios.post.firstCall, '/reference-data-to-update', imageIds); assert.calledWith(axios.post.secondCall, '/undo-accept-images', images); assert.calledWith(dispatch, { - type: actionNames.UNDO_ACCEPT_IMAGES, - payload: {skipTreeUpdate: false} + type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, + payload: {} }); }); - - it('should skip tree update, if specified', async () => { - const imageIds = ['img-id-1', 'img-id-2']; - const data = {foo: 'bar'}; - axios.post.withArgs('/undo-accept-images', imageIds).returns({data}); - - await undoAcceptImages(imageIds, {skipTreeUpdate: true})(dispatch); - - assert.calledWith(dispatch, { - type: actionNames.UNDO_ACCEPT_IMAGES, - payload: {skipTreeUpdate: true} - }); - }); - }); - - describe('retryTest', () => { - it('should retry passed test', async () => { - const test = {testName: 'test-name', browserName: 'yabro'}; - - await retryTest(test)(dispatch); - - assert.calledOnceWith(axios.post, '/run', [test]); - assert.calledOnceWith(dispatch, {type: actionNames.RETRY_TEST}); - }); - }); - - describe('runFailedTests', () => { - it('should run all failed tests', async () => { - const failedTests = [ - {testName: 'test-name-1', browserName: 'yabro'}, - {testName: 'test-name-2', browserName: 'yabro'} - ]; - - await runFailedTests(failedTests)(dispatch); - - assert.calledOnceWith(axios.post, '/run', failedTests); - assert.calledOnceWith(dispatch, {type: actionNames.RUN_FAILED_TESTS}); - }); - }); - - describe('runCustomGuiAction', () => { - it('should run custom action on server for control of given group of section', async () => { - const payload = { - sectionName: 'foo', - groupIndex: 100, - controlIndex: 500 - }; - - await actions.runCustomGuiAction(payload)(dispatch); - - assert.calledOnceWith( - axios.post, - sinon.match.any, - sinon.match(({sectionName, groupIndex, controlIndex}) => { - assert.equal(sectionName, 'foo'); - assert.equal(groupIndex, 100); - assert.equal(controlIndex, 500); - return true; - }) - ); - }); - - it('should dispatch action for control of given group of section', async () => { - const payload = { - sectionName: 'foo', - groupIndex: 100, - controlIndex: 500 - }; - - await actions.runCustomGuiAction(payload)(dispatch); - - assert.calledOnceWith( - dispatch, - { - type: actionNames.RUN_CUSTOM_GUI_ACTION, - payload: { - sectionName: 'foo', - groupIndex: 100, - controlIndex: 500 - } - } - ); - }); - - it('should show notification if error in action on the server is happened', async () => { - const payload = { - sectionName: 'foo', - groupIndex: 100, - controlIndex: 500 - }; - axios.post.throws(new Error('failed to run custom gui control action')); - - await actions.runCustomGuiAction(payload)(dispatch); - - assert.calledOnceWith( - notify, - { - dismissAfter: 0, - id: 'runCustomGuiAction', - message: 'failed to run custom gui control action', - status: 'error', - position: POSITIONS.topCenter, - dismissible: true, - showDismissButton: true, - allowHTML: true - } - ); - }); }); describe('openModal', () => { @@ -183,67 +69,4 @@ describe('lib/static/modules/actions', () => { assert.deepEqual(actions.closeModal(modal), {type: actionNames.CLOSE_MODAL, payload: modal}); }); }); - - describe('testsEnd', () => { - it('should connect to database', async () => { - const href = 'http://127.0.0.1:8080/sqlite.db'; - getMainDatabaseUrl.returns({href}); - - await actions.testsEnd()(dispatch); - - assert.calledOnceWith(connectToDatabaseStub, href); - }); - - it('should dispatch "TESTS_END" action with db connection', async () => { - const db = {}; - connectToDatabaseStub.resolves(db); - - await actions.testsEnd()(dispatch); - - assert.calledOnceWith(dispatch, { - type: actionNames.TESTS_END, - payload: {db} - }); - }); - - it('should show notification if error appears', async () => { - connectToDatabaseStub.rejects(new Error('failed to connect to database')); - - await actions.testsEnd()(dispatch); - - assert.calledOnceWith( - notify, - { - dismissAfter: 0, - id: 'testsEnd', - message: 'failed to connect to database', - status: 'error', - position: POSITIONS.topCenter, - dismissible: true, - showDismissButton: true, - allowHTML: true - } - ); - }); - }); - - describe('changeDiffMode', () => { - [ - {mode: DiffModes.THREE_UP.id, actionType: actionNames.VIEW_THREE_UP_DIFF}, - {mode: DiffModes.THREE_UP_SCALED.id, actionType: actionNames.VIEW_THREE_UP_SCALED_DIFF}, - {mode: DiffModes.THREE_UP_SCALED_TO_FIT.id, actionType: actionNames.VIEW_THREE_UP_SCALED_TO_FIT_DIFF}, - {mode: DiffModes.ONLY_DIFF.id, actionType: actionNames.VIEW_ONLY_DIFF}, - {mode: DiffModes.SWITCH.id, actionType: actionNames.VIEW_SWITCH_DIFF}, - {mode: DiffModes.SWIPE.id, actionType: actionNames.VIEW_SWIPE_DIFF}, - {mode: DiffModes.ONION_SKIN.id, actionType: actionNames.VIEW_ONION_SKIN_DIFF}, - {mode: 'UNKNOWN_MODE', actionType: actionNames.VIEW_THREE_UP_DIFF} - ].forEach(({mode, actionType}) => { - it(`should dispatch "${actionType}" action`, () => { - assert.deepEqual( - actions.changeDiffMode(mode), - {type: actionType} - ); - }); - }); - }); }); diff --git a/test/unit/lib/static/modules/actions/lifecycle.ts b/test/unit/lib/static/modules/actions/lifecycle.ts index 3026a3f55..ae7d3be8b 100644 --- a/test/unit/lib/static/modules/actions/lifecycle.ts +++ b/test/unit/lib/static/modules/actions/lifecycle.ts @@ -1,7 +1,6 @@ import axiosOriginal from 'axios'; import _ from 'lodash'; import proxyquire from 'proxyquire'; -import {POSITIONS} from 'reapop'; import sinon, {SinonStub, SinonStubbedInstance} from 'sinon'; import {LOCAL_DATABASE_NAME, ToolName} from '@/constants'; @@ -11,26 +10,26 @@ import type * as actionsModule from '@/static/modules/actions/lifecycle'; const axios = axiosOriginal as unknown as SinonStubbedInstance; -describe('lib/static/modules/actions', () => { +describe('lib/static/modules/actions/lifecycle', () => { const sandbox = sinon.createSandbox(); let actions: typeof actionsModule; let dispatch: SinonStub; let getMainDatabaseUrl: SinonStub; let connectToDatabaseStub: SinonStub; - let notify: SinonStub; + let createNotificationError: SinonStub; let pluginsStub: {loadAll: SinonStub}; beforeEach(() => { dispatch = sandbox.stub(); - notify = sandbox.stub(); + createNotificationError = sandbox.stub(); getMainDatabaseUrl = sandbox.stub().returns({href: 'http://localhost/default/sqlite.db'}); connectToDatabaseStub = sandbox.stub().resolves({}); pluginsStub = {loadAll: sandbox.stub()}; actions = proxyquire('lib/static/modules/actions/lifecycle', { - '@/static/modules/actions/index': proxyquire('@/static/modules/actions/index', { - 'reapop': {notify} - }), + '@/static/modules/actions/notifications': { + createNotificationError + }, '@/db-utils/client': {getMainDatabaseUrl, connectToDatabase: connectToDatabaseStub}, '@/static/modules/plugins': pluginsStub }); @@ -72,23 +71,12 @@ describe('lib/static/modules/actions', () => { }); it('should show notification if error in initialization on the server is happened', async () => { - axios.get.rejects(new Error('failed to initialize custom gui')); + const customGuiError = new Error('failed to initialize custom gui'); + axios.get.rejects(customGuiError); await actions.thunkInitGuiReport()(dispatch, sinon.stub(), sinon.stub()); - assert.calledOnceWith( - notify, - { - dismissAfter: 0, - id: 'initGuiReport', - message: 'failed to initialize custom gui', - status: 'error', - position: POSITIONS.topCenter, - dismissible: true, - showDismissButton: true, - allowHTML: true - } - ); + assert.calledOnceWith(createNotificationError, 'initGuiReport', customGuiError); }); it('should init plugins with the config from /init route', async () => { diff --git a/test/unit/lib/static/modules/actions/run-tests.ts b/test/unit/lib/static/modules/actions/run-tests.ts new file mode 100644 index 000000000..bcaeae0f7 --- /dev/null +++ b/test/unit/lib/static/modules/actions/run-tests.ts @@ -0,0 +1,97 @@ +import * as runTestsActions from '@/static/modules/actions/run-tests'; +import actionNames from '@/static/modules/action-names'; +import sinon, {SinonStub, SinonStubbedInstance} from 'sinon'; +import proxyquire from 'proxyquire'; +import axiosOriginal from 'axios'; + +const axios = axiosOriginal as unknown as SinonStubbedInstance; + +describe('lib/static/modules/actions/run-tests', () => { + const sandbox = sinon.sandbox.create(); + let getMainDatabaseUrl: SinonStub; + let connectToDatabaseStub: SinonStub; + let createNotificationErrorStub: SinonStub; + let dispatch: SinonStub; + let actions: typeof runTestsActions; + + beforeEach(() => { + dispatch = sandbox.stub(); + getMainDatabaseUrl = sandbox.stub().returns({href: 'http://localhost/default/sqlite.db'}); + connectToDatabaseStub = sandbox.stub().resolves({}); + createNotificationErrorStub = sandbox.stub(); + + sandbox.stub(axios, 'post').resolves({data: {}}); + + actions = proxyquire('lib/static/modules/actions/run-tests', { + '@/db-utils/client': {getMainDatabaseUrl, connectToDatabase: connectToDatabaseStub}, + '@/static/modules/actions/notifications': {createNotificationError: createNotificationErrorStub} + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('thunkRunTest', () => { + it('should retry passed test', async () => { + dispatch.callsFake((fn) => typeof fn === 'function' ? fn() : fn); + const test = {testName: 'test-name', browserName: 'yabro'}; + + await actions.thunkRunTest({test})(dispatch, sinon.stub(), null); + + assert.calledOnceWith(axios.post, '/run', [test]); + assert.calledWith(dispatch, {type: actionNames.RETRY_TEST}); + }); + }); + + describe('thunkRunFailedTests', () => { + it('should run all failed tests', async () => { + dispatch.callsFake((fn) => typeof fn === 'function' ? fn() : fn); + const failedTests = [ + {testName: 'test-name-1', browserName: 'yabro'}, + {testName: 'test-name-2', browserName: 'yabro'} + ]; + + await actions.thunkRunFailedTests({tests: failedTests})(dispatch, sinon.stub(), null); + + assert.calledOnceWith(axios.post, '/run', failedTests); + assert.calledWith(dispatch, {type: actionNames.RUN_FAILED_TESTS}); + }); + }); + + describe('testsEnd', () => { + it('should connect to database', async () => { + const href = 'http://127.0.0.1:8080/sqlite.db'; + getMainDatabaseUrl.returns({href}); + + await actions.thunkTestsEnd()(dispatch, sinon.stub(), null); + + assert.calledOnceWith(connectToDatabaseStub, href); + }); + + it('should dispatch "TESTS_END" action with db connection', async () => { + const db = {}; + connectToDatabaseStub.resolves(db); + + await actions.thunkTestsEnd()(dispatch, sinon.stub(), null); + + assert.calledOnceWith(dispatch, { + type: actionNames.TESTS_END, + payload: {db} + }); + }); + + it('should show notification if error appears', async () => { + const dbConnectError = new Error('failed to connect to database'); + connectToDatabaseStub.rejects(dbConnectError); + + await actions.thunkTestsEnd()(dispatch, sinon.stub(), null); + + assert.calledOnceWith( + createNotificationErrorStub, + 'testsEnd', + dbConnectError + ); + }); + }); +}); diff --git a/test/unit/lib/static/modules/middlewares/metrika.js b/test/unit/lib/static/modules/middlewares/metrika.js index b9ed4fffa..cb396920b 100644 --- a/test/unit/lib/static/modules/middlewares/metrika.js +++ b/test/unit/lib/static/modules/middlewares/metrika.js @@ -1,4 +1,3 @@ -import YandexMetrika from 'lib/static/modules/yandex-metrika'; import actionNames from 'lib/static/modules/action-names'; import proxyquire from 'proxyquire'; @@ -7,7 +6,7 @@ globalThis.performance = globalThis.performance; // node v14 stub describe('lib/static/modules/middlewares/metrika', () => { const sandbox = sinon.sandbox.create(); - let next, metrikaMiddleware, measurePerformanceStub; + let next, getMetrikaMiddleware, measurePerformanceStub, analyticsStub; const mkStore_ = (state = {}) => { return {getState: sandbox.stub().returns(state)}; @@ -21,23 +20,25 @@ describe('lib/static/modules/middlewares/metrika', () => { }; const action = {type: eventName, payload}; - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); }; beforeEach(() => { next = sandbox.stub(); measurePerformanceStub = sandbox.stub(); - metrikaMiddleware = proxyquire.noPreserveCache().noCallThru()('lib/static/modules/middlewares/metrika', { + getMetrikaMiddleware = proxyquire.noPreserveCache().noCallThru()('lib/static/modules/middlewares/metrika', { '../web-vitals': { measurePerformance: measurePerformanceStub } - }).default; + }).getMetrikaMiddleware; - sandbox.stub(YandexMetrika, 'create').returns(Object.create(YandexMetrika.prototype)); - sandbox.stub(YandexMetrika.prototype, 'acceptScreenshot'); - sandbox.stub(YandexMetrika.prototype, 'acceptOpenedScreenshots'); - sandbox.stub(YandexMetrika.prototype, 'sendVisitParams'); + analyticsStub = { + setVisitParams: sinon.stub(), + trackScreenshotsAccept: sinon.stub(), + trackOpenedScreenshotsAccept: sinon.stub(), + trackFeatureUsage: sinon.stub() + }; }); afterEach(() => sandbox.restore()); @@ -46,280 +47,107 @@ describe('lib/static/modules/middlewares/metrika', () => { const store = mkStore_(); const action = {type: 'FOO_BAR'}; - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); assert.calledOnceWith(next, action); }); [actionNames.INIT_GUI_REPORT, actionNames.INIT_STATIC_REPORT].forEach((eventName) => { describe(`"${eventName}" event`, () => { - describe('"counterNumber" is not specified in y.metrika config', () => { - let store, payload; - - beforeEach(() => { - store = mkStore_(); - payload = {}; - }); + let store, payload; - it('should call next middleware with passed action', () => { - const action = {type: eventName, payload}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledOnceWith(next, action); - }); + beforeEach(() => { + store = mkStore_(); + payload = {config: { + yandexMetrika: {counterNumber: 100500} + }}; + }); - it('should not create y.metrika instance', () => { - const action = {type: eventName, payload}; + it('should call next middleware with passed action', () => { + const action = {type: eventName, payload}; - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.notCalled(YandexMetrika.create); - }); + assert.calledOnceWith(next, action); + }); - it('should not measure site performance', () => { - const action = {type: eventName, payload}; + it('should call next middleware before get state', () => { + const action = {type: eventName, payload}; - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.notCalled(measurePerformanceStub); - }); + assert.callOrder(next, store.getState); }); - describe('"counterNumber" is specified in y.metrika config', () => { - let store, payload; - - beforeEach(() => { - store = mkStore_(); - payload = {config: { - yandexMetrika: {counterNumber: 100500} - }}; - }); - - it('should call next middleware with passed action', () => { + describe('measure site performance', () => { + it('should send some performance info to y.metrika with rounded value', () => { const action = {type: eventName, payload}; + measurePerformanceStub.callsFake(cb => cb({name: 'XXX', value: 100.999})); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.calledOnceWith(next, action); + assert.calledWith(analyticsStub.setVisitParams.firstCall, {XXX: 101}); }); - it('should call next middleware before get state', () => { + it('should send "CLS" performance info to y.metrika with multiplied by 1000 value', () => { const action = {type: eventName, payload}; + measurePerformanceStub.callsFake(cb => cb({name: 'CLS', value: 0.99999})); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.callOrder(next, store.getState); + assert.calledWith(analyticsStub.setVisitParams.firstCall, {CLS: 1000}); }); + }); - it('should create y.metrika instance with passed config', () => { - const action = {type: eventName, payload}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); + describe('send visit parameters to metrika', () => { + let clock; - assert.calledOnceWith(YandexMetrika.create, {counterNumber: 100500}); + beforeEach(() => { + clock = sinon.useFakeTimers(); }); - describe('measure site performance', () => { - it('should send some performance info to y.metrika with rounded value', () => { - const action = {type: eventName, payload}; - measurePerformanceStub.callsFake(cb => cb({name: 'XXX', value: 100.999})); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); + afterEach(() => clock.restore()); - assert.calledWith(YandexMetrika.prototype.sendVisitParams.firstCall, {XXX: 101}); - }); - - it('should send "CLS" performance info to y.metrika with multiplied by 1000 value', () => { - const action = {type: eventName, payload}; - measurePerformanceStub.callsFake(cb => cb({name: 'CLS', value: 0.99999})); + it(`should send "${eventName}" render time`, () => { + const store = mkStore_(); + const action = {type: eventName, payload}; + next = () => clock.tick(5000); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.calledWith(YandexMetrika.prototype.sendVisitParams.firstCall, {CLS: 1000}); - }); + assert.calledOnceWith(analyticsStub.setVisitParams, sinon.match({[eventName]: 5000})); }); - describe('send visit parameters to metrika', () => { - let clock; - - beforeEach(() => { - clock = sinon.useFakeTimers(); - }); - - afterEach(() => clock.restore()); - - it(`should send "${eventName}" render time`, () => { - const store = mkStore_(); - const action = {type: eventName, payload}; - next = () => clock.tick(5000); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledOnceWith(YandexMetrika.prototype.sendVisitParams, sinon.match({[eventName]: 5000})); - }); - - it('should send "testsCount"', () => { - const store = mkStore_({ - tree: { - browsers: { - allIds: ['a', 'b', 'c'] - } + it('should send "testsCount"', () => { + const store = mkStore_({ + tree: { + browsers: { + allIds: ['a', 'b', 'c'] } - }); - const action = {type: eventName, payload}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledOnceWith(YandexMetrika.prototype.sendVisitParams, sinon.match({testsCount: 3})); + } }); + const action = {type: eventName, payload}; - it('should send "view" params', () => { - const store = mkStore_({ - view: {foo: 'bar', baz: 'qux'} - }); - const action = {type: eventName, payload}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.calledOnceWith( - YandexMetrika.prototype.sendVisitParams, - sinon.match({initView: {foo: 'bar', baz: 'qux'}}) - ); - }); + assert.calledOnceWith(analyticsStub.setVisitParams, sinon.match({testsCount: 3})); }); - }); - }); - }); - - describe(`"${actionNames.ACCEPT_SCREENSHOT}" event`, () => { - describe('if metrika is not inited', () => { - it('should not register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.acceptScreenshot); - }); - - it('should not send counter id', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.sendVisitParams); - }); - }); - - it('should register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledWith(YandexMetrika.prototype.acceptScreenshot); - }); - it('should send counter id', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledWith( - YandexMetrika.prototype.sendVisitParams, - {counterId: actionNames.ACCEPT_SCREENSHOT} - ); - }); - - it('should call next middleware after register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.callOrder(YandexMetrika.prototype.acceptScreenshot, next); - }); - - it('should call next middleware with passed action', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_SCREENSHOT}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledWith(next, action); - }); - }); - - describe(`"${actionNames.ACCEPT_OPENED_SCREENSHOTS}" event`, () => { - describe('if metrika is not inited', () => { - it('should not register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS, payload: [{}, {}]}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.acceptOpenedScreenshots); - }); - - it('should not send counter id', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS}; + it('should send "view" params', () => { + const store = mkStore_({ + view: {foo: 'bar', baz: 'qux'} + }); + const action = {type: eventName, payload}; - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); - assert.notCalled(YandexMetrika.prototype.sendVisitParams); + assert.calledOnceWith( + analyticsStub.setVisitParams, + sinon.match({initView: {foo: 'bar', baz: 'qux'}}) + ); + }); }); }); - - it('should register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS, payload: [{}, {}]}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledOnceWith(YandexMetrika.prototype.acceptOpenedScreenshots, {acceptedImagesCount: 2}); - }); - - it('should send counter id', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledWith( - YandexMetrika.prototype.sendVisitParams, - {counterId: actionNames.ACCEPT_OPENED_SCREENSHOTS} - ); - }); - - it('should call next middleware after register goal', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS, payload: [{}, {}]}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.callOrder(YandexMetrika.prototype.acceptOpenedScreenshots, next); - }); - - it('should call next middleware with passed action', () => { - const store = mkStore_(); - const action = {type: actionNames.ACCEPT_OPENED_SCREENSHOTS, payload: [{}, {}]}; - initReportWithMetrikaCounter(); - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.calledWith(next, action); - }); }); [ @@ -333,46 +161,25 @@ describe('lib/static/modules/middlewares/metrika', () => { actionNames.VIEW_EXPAND_ERRORS, actionNames.VIEW_EXPAND_RETRIES, actionNames.VIEW_UPDATE_BASE_HOST, - actionNames.VIEW_THREE_UP_DIFF, - actionNames.VIEW_THREE_UP_SCALED_DIFF, - actionNames.VIEW_THREE_UP_SCALED_TO_FIT_DIFF, - actionNames.VIEW_ONLY_DIFF, - actionNames.VIEW_SWITCH_DIFF, - actionNames.VIEW_SWIPE_DIFF, - actionNames.VIEW_ONION_SKIN_DIFF, actionNames.BROWSERS_SELECTED, actionNames.VIEW_UPDATE_FILTER_BY_NAME, actionNames.VIEW_SET_STRICT_MATCH_FILTER, actionNames.RUN_CUSTOM_GUI_ACTION, - actionNames.COPY_SUITE_NAME, - actionNames.VIEW_IN_BROWSER, - actionNames.COPY_TEST_LINK, actionNames.TOGGLE_SUITE_SECTION, actionNames.TOGGLE_BROWSER_SECTION, - actionNames.GROUP_TESTS_BY_KEY, - actionNames.APPLY_DELAYED_TEST_RESULTS + actionNames.GROUP_TESTS_BY_KEY ].forEach((eventName) => { describe(`"${eventName}" event`, () => { - it('should not send counter id if metrika is not inited', () => { - const store = mkStore_(); - const action = {type: eventName}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.sendVisitParams); - assert.calledWith(next, action); - }); - - it('should send counter id', () => { + it('should track feature usage', () => { const store = mkStore_(); const action = {type: eventName}; initReportWithMetrikaCounter(); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); assert.calledWith( - YandexMetrika.prototype.sendVisitParams, - {counterId: eventName} + analyticsStub.trackFeatureUsage, + {featureName: eventName} ); assert.calledWith(next, action); }); @@ -384,26 +191,16 @@ describe('lib/static/modules/middlewares/metrika', () => { actionNames.CLOSE_MODAL ].forEach((eventName) => { describe(`"${eventName}" event`, () => { - it('should not send counter id if metrika is not inited', () => { - const store = mkStore_(); - const action = {type: eventName, payload: {id: 'foo-bar'}}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.sendVisitParams); - assert.calledWith(next, action); - }); - - it('should send counter id from payload', () => { + it('should track feature usage', () => { const store = mkStore_(); const action = {type: eventName, payload: {id: 'foo-bar'}}; initReportWithMetrikaCounter(); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); assert.calledWith( - YandexMetrika.prototype.sendVisitParams, - {counterId: 'foo-bar'} + analyticsStub.trackFeatureUsage, + {featureName: 'foo-bar'} ); assert.calledWith(next, action); }); @@ -415,28 +212,16 @@ describe('lib/static/modules/middlewares/metrika', () => { actionNames.CHANGE_TEST_RETRY ].forEach((eventName) => { describe(`"${eventName}" event`, () => { - describe('should not send counter id if', () => { - it('metrika is not inited', () => { - const store = mkStore_(); - const action = {type: eventName}; - - metrikaMiddleware(YandexMetrika)(store)(next)(action); - - assert.notCalled(YandexMetrika.prototype.sendVisitParams); - assert.calledWith(next, action); - }); - }); - - it('should send counter id', () => { + it('should track feature usage', () => { const store = mkStore_(); const action = {type: eventName}; initReportWithMetrikaCounter(); - metrikaMiddleware(YandexMetrika)(store)(next)(action); + getMetrikaMiddleware(analyticsStub)(store)(next)(action); assert.calledWith( - YandexMetrika.prototype.sendVisitParams, - {counterId: eventName} + analyticsStub.trackFeatureUsage, + {featureName: eventName} ); assert.calledWith(next, action); }); 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 76c5de1b4..348494ce1 100644 --- a/test/unit/lib/static/modules/reducers/grouped-tests/index.js +++ b/test/unit/lib/static/modules/reducers/grouped-tests/index.js @@ -41,8 +41,8 @@ describe('lib/static/modules/reducers/grouped-tests', () => { actionNames.VIEW_UPDATE_FILTER_BY_NAME, actionNames.VIEW_SET_STRICT_MATCH_FILTER, actionNames.CHANGE_VIEW_MODE, - actionNames.ACCEPT_SCREENSHOT, - actionNames.ACCEPT_OPENED_SCREENSHOTS + actionNames.COMMIT_ACCEPTED_IMAGES_TO_TREE, + actionNames.COMMIT_REVERTED_IMAGES_TO_TREE ].forEach((actionName) => { describe(`${actionName} action`, () => { it('should calc only available meta keys if tests are not grouped', () => { diff --git a/test/unit/lib/static/modules/reducers/tree/index.js b/test/unit/lib/static/modules/reducers/tree/index.js index d701f082d..3c8ce4757 100644 --- a/test/unit/lib/static/modules/reducers/tree/index.js +++ b/test/unit/lib/static/modules/reducers/tree/index.js @@ -698,7 +698,7 @@ describe('lib/static/modules/reducers/tree', () => { }); }); - [actionNames.TEST_BEGIN, actionNames.TEST_RESULT, actionNames.ACCEPT_OPENED_SCREENSHOTS, actionNames.ACCEPT_SCREENSHOT].forEach((actionName) => { + [actionNames.TEST_BEGIN, actionNames.TEST_RESULT, actionNames.COMMIT_ACCEPTED_IMAGES_TO_TREE].forEach((actionName) => { describe(`${actionName} action`, () => { it('should change "retryIndex" in browser state', () => { const suitesById = {...mkSuite({id: 's1', browserIds: ['b1']})}; @@ -1274,7 +1274,7 @@ describe('lib/static/modules/reducers/tree', () => { }); }); - describe(`${actionNames.UNDO_ACCEPT_IMAGES} action`, () => { + describe(`${actionNames.COMMIT_REVERTED_IMAGES_TO_TREE} action`, () => { const mkTree_ = () => mkStateTree({ suitesById: mkSuite({id: 's', status: SUCCESS, browserIds: ['b']}), browsersById: mkBrowser({id: 'b', name: 'yabro', parentId: 's', resultIds: ['r1']}), @@ -1296,7 +1296,7 @@ describe('lib/static/modules/reducers/tree', () => { const updatedImage = {id: 'i1', parentId: 'r1', status: FAIL}; const {tree: newTree} = reducer({tree, view}, { - type: actionNames.UNDO_ACCEPT_IMAGES, + type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, payload: {updatedImages: [updatedImage]} }); @@ -1309,7 +1309,7 @@ describe('lib/static/modules/reducers/tree', () => { const updatedImage = {id: 'i1', parentId: 'r1', status: FAIL}; const {tree: newTree} = reducer({tree, view}, { - type: actionNames.UNDO_ACCEPT_IMAGES, + type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, payload: {updatedImages: [updatedImage]} }); @@ -1321,7 +1321,7 @@ describe('lib/static/modules/reducers/tree', () => { const view = mkStateView(); const {tree: newTree} = reducer({tree, view}, { - type: actionNames.UNDO_ACCEPT_IMAGES, + type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, payload: {removedResults: ['r2']} }); @@ -1342,29 +1342,13 @@ describe('lib/static/modules/reducers/tree', () => { const updatedImage = {id: 'i1', parentId: 'r1', status: FAIL}; const {tree: newTree} = reducer({tree, view}, { - type: actionNames.UNDO_ACCEPT_IMAGES, + type: actionNames.COMMIT_REVERTED_IMAGES_TO_TREE, payload: {updatedImages: [updatedImage]} }); assert.equal(newTree.suites.byId.s.status, FAIL); assert.deepEqual(newTree.suites.failedRootIds, ['s']); }); - - it('should not modify state if "skipTreeUpdate" is set', () => { - const state = {tree: mkTree_(), view: mkStateView()}; - const updatedImage = {id: 'i1', status: FAIL}; - - const newState = reducer(state, { - type: actionNames.UNDO_ACCEPT_IMAGES, - payload: { - updatedImages: [updatedImage], - removedResults: ['r2'], - skipTreeUpdate: true - } - }); - - assert.equal(state, newState); - }); }); describe(`${actionNames.TOGGLE_BROWSER_CHECKBOX} action`, () => { diff --git a/test/unit/lib/static/modules/web-vitals.js b/test/unit/lib/static/modules/web-vitals.js index 99924f034..35d7269b0 100644 --- a/test/unit/lib/static/modules/web-vitals.js +++ b/test/unit/lib/static/modules/web-vitals.js @@ -16,12 +16,6 @@ describe('WebVitals', () => { describe('measurePerformance', () => { ['getCLS', 'getFID', 'getFCP', 'getLCP', 'getTTFB'].forEach((methodName) => { - it(`should not call "${methodName}" if callback is not passed`, () => { - measurePerformance(); - - assert.notCalled(webVitals[methodName]); - }); - it(`should call "${methodName}" if callback is passed`, () => { const spy = sinon.spy(); diff --git a/test/unit/lib/static/modules/yandex-metrika.js b/test/unit/lib/static/modules/yandex-metrika.js index fa98f0020..95033df21 100644 --- a/test/unit/lib/static/modules/yandex-metrika.js +++ b/test/unit/lib/static/modules/yandex-metrika.js @@ -1,4 +1,4 @@ -import YandexMetrika from 'lib/static/modules/yandex-metrika'; +import {YandexMetrika} from 'lib/static/modules/yandex-metrika'; describe('YandexMetrika', () => { const sandbox = sinon.createSandbox(); @@ -14,27 +14,25 @@ describe('YandexMetrika', () => { sandbox.restore(); }); - ['acceptScreenshot', 'acceptOpenedScreenshots', 'sendVisitParams'].forEach((methodName) => { + ['setVisitParams', 'trackScreenshotsAccept', 'trackOpenedScreenshotsAccept', 'trackFeatureUsage'].forEach((methodName) => { describe(`"${methodName}" method`, () => { - describe('should not send anything to yandex metrika if', () => { - it('"counterNumber" is not a number', () => { - const yMetrika = YandexMetrika.create({counterNumber: null}); + it('should not send anything to yandex metrika if disabled', () => { + const yMetrika = new YandexMetrika(false, 0); - yMetrika[methodName](); + yMetrika[methodName](); - assert.notCalled(global.window.ym); - }); + assert.notCalled(global.window.ym); }); }); }); [ - {methodName: 'acceptScreenshot', target: 'ACCEPT_SCREENSHOT'}, - {methodName: 'acceptOpenedScreenshots', target: 'ACCEPT_OPENED_SCREENSHOTS'} + {methodName: 'trackScreenshotsAccept', target: 'ACCEPT_SCREENSHOT'}, + {methodName: 'trackOpenedScreenshotsAccept', target: 'ACCEPT_OPENED_SCREENSHOTS'} ].forEach(({methodName, target}) => { describe(`"${methodName}" method`, () => { it('should register "${method}" goal', () => { - const yMetrika = YandexMetrika.create({counterNumber: 100500}); + const yMetrika = new YandexMetrika(true, 100500); yMetrika[methodName]({acceptedImagesCount: 1}); @@ -43,11 +41,11 @@ describe('YandexMetrika', () => { }); }); - describe('"sendVisitParams" method', () => { + describe('"setVisitParams" method', () => { it(`should send all passed parameters`, () => { - const yMetrika = YandexMetrika.create({counterNumber: 100500}); + const yMetrika = new YandexMetrika(true, 100500); - yMetrika.sendVisitParams({foo: 10, bar: 20}); + yMetrika.setVisitParams({foo: 10, bar: 20}); assert.calledOnceWith(global.window.ym, 100500, 'params', {foo: 10, bar: 20}); }); From 4a4c2bffdbf42b2e9dc11a270a71ba5fd50e4e3d Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 3 Jan 2025 20:31:45 +0300 Subject: [PATCH 2/2] fix: fix static accepter action naming --- lib/static/modules/actions/static-accepter.ts | 2 +- lib/static/modules/actions/types.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/static/modules/actions/static-accepter.ts b/lib/static/modules/actions/static-accepter.ts index a89cc03fd..665d54268 100644 --- a/lib/static/modules/actions/static-accepter.ts +++ b/lib/static/modules/actions/static-accepter.ts @@ -145,7 +145,7 @@ export const staticAccepterUpdateCommitMessage = (payload: {commitMessage: strin return {type: actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE, payload}; }; -export type StaticAccepter = +export type StaticAccepterAction = | StaticAccepterDelayScreenshotAction | StaticAccepterUndoDelayScreenshotAction | StaticAccepterStageScreenshotAction diff --git a/lib/static/modules/actions/types.ts b/lib/static/modules/actions/types.ts index ca66bd724..76bd01564 100644 --- a/lib/static/modules/actions/types.ts +++ b/lib/static/modules/actions/types.ts @@ -17,7 +17,7 @@ import {CustomGuiAction} from '@/static/modules/actions/custom-gui'; import {FilterTestsAction} from '@/static/modules/actions/filter-tests'; import {SettingsAction} from '@/static/modules/actions/settings'; import {ProcessingAction} from '@/static/modules/actions/processing'; -import {StaticAccepter} from '@/static/modules/actions/static-accepter'; +import {StaticAccepterAction} from '@/static/modules/actions/static-accepter'; import type actionNames from '../action-names'; import type defaultState from '../default-state'; import type {Tree} from '../../../tests-tree-builder/base'; @@ -44,6 +44,6 @@ export type SomeAction = | ScreenshotsAction | SettingsAction | SortTestsAction - | StaticAccepter + | StaticAccepterAction | SuitesPageAction | SuiteTreeStateAction;