Skip to content

Commit

Permalink
feat: add static image accepter support to new ui (#612)
Browse files Browse the repository at this point in the history
* feat: add static image accepter support to new ui
  • Loading branch information
shadowusr authored Nov 7, 2024
1 parent eae6670 commit f70e78b
Show file tree
Hide file tree
Showing 25 changed files with 649 additions and 149 deletions.
7 changes: 6 additions & 1 deletion lib/static/modules/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export default {
STATIC_ACCEPTER_STAGE_SCREENSHOT: 'STATIC_ACCEPTER_STAGE_SCREENSHOT',
STATIC_ACCEPTER_UNSTAGE_SCREENSHOT: 'STATIC_ACCEPTER_UNSTAGE_SCREENSHOT',
STATIC_ACCEPTER_COMMIT_SCREENSHOT: 'STATIC_ACCEPTER_COMMIT_SCREENSHOT',
STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET: 'STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET',
STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE: 'STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE',
CLOSE_SECTIONS: 'CLOSE_SECTIONS',
TOGGLE_STATE_RESULT: 'TOGGLE_STATE_RESULT',
TOGGLE_LOADING: 'TOGGLE_LOADING',
Expand Down Expand Up @@ -64,5 +66,8 @@ export default {
SUITES_PAGE_SET_SECTION_EXPANDED: 'SUITES_PAGE_SET_SECTION_EXPANDED',
SUITES_PAGE_SET_STEPS_EXPANDED: 'SUITES_PAGE_SET_STEPS_EXPANDED',
VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 'VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE',
UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS'
UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS',
UPDATE_LOADING_VISIBILITY: 'UPDATE_LOADING_VISIBILITY',
UPDATE_LOADING_TITLE: 'UPDATE_LOADING_TITLE',
UPDATE_LOADING_IS_IN_PROGRESS: 'UPDATE_LOADING_IS_IN_PROGRESS'
} as const;
45 changes: 39 additions & 6 deletions lib/static/modules/actions/static-accepter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import actionNames from '../action-names';
import defaultState from '../default-state';
import type {Action, Dispatch, Store} from './types';
import {ThunkAction} from 'redux-thunk';
import {Point} from '@/static/new-ui/types';

type StaticAccepterDelayScreenshotPayload = {imageId: string, stateName: string, stateNameImageId: string}[];
type StaticAccepterDelayScreenshotAction = Action<typeof actionNames.STATIC_ACCEPTER_DELAY_SCREENSHOT, StaticAccepterDelayScreenshotPayload>
Expand Down Expand Up @@ -47,6 +48,10 @@ type StaticAccepterConfig = typeof defaultState['config']['staticImageAccepter']
type StaticAccepterPayload = {id: string, stateNameImageId: string, image: string, path: string}[];
type StaticAccepterCommitScreenshotOptions = Pick<StaticAccepterConfig, 'repositoryUrl' | 'pullRequestUrl' | 'serviceUrl' | 'axiosRequestOptions' | 'meta'> & {message: string};

export interface CommitResult {
error?: Error;
}

type StaticAccepterCommitScreenshotAction = Action<typeof actionNames.STATIC_ACCEPTER_COMMIT_SCREENSHOT, string[]>;
export const staticAccepterCommitScreenshot = (
imagesInfo: StaticAccepterPayload,
Expand All @@ -58,10 +63,13 @@ export const staticAccepterCommitScreenshot = (
axiosRequestOptions = {},
meta
}: StaticAccepterCommitScreenshotOptions
): ThunkAction<Promise<void>, Store, void, StaticAccepterCommitScreenshotAction> => {
return async (dispatch: Dispatch) => {
): ThunkAction<Promise<CommitResult>, Store, void, StaticAccepterCommitScreenshotAction> => {
return async (dispatch: Dispatch): Promise<CommitResult> => {
dispatch({type: actionNames.PROCESS_BEGIN});
dispatch(staticAccepterCloseConfirm());
dispatch({type: actionNames.UPDATE_LOADING_IS_IN_PROGRESS, payload: true});
dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: `Preparing images to commit: 0 of ${imagesInfo.length}`});
dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: true});

try {
const payload = new FormData();
Expand All @@ -74,13 +82,21 @@ export const staticAccepterCommitScreenshot = (
payload.append('meta', JSON.stringify(meta));
}

await Promise.all(imagesInfo.map(async imageInfo => {
await Promise.all(imagesInfo.map(async (imageInfo, index) => {
dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: `Preparing images to commit: ${index + 1} of ${imagesInfo.length}`});

const blob = await getBlob(imageInfo.image);

payload.append('image', blob, imageInfo.path);
}));

const response = await axios.post(serviceUrl, payload, axiosRequestOptions);
dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: 'Uploading images'});
const response = await axios.post(serviceUrl, payload, {
...axiosRequestOptions,
onUploadProgress: (e) => {
dispatch({type: actionNames.UPDATE_LOADING_PROGRESS, payload: {'static-accepter-commit': e.loaded / (e.total ?? e.loaded)}});
}
});

const commitedImageIds = imagesInfo.map(imageInfo => imageInfo.id);
const commitedImages = imagesInfo.map(imageInfo => ({
Expand All @@ -93,20 +109,37 @@ export const staticAccepterCommitScreenshot = (

storeCommitInLocalStorage(commitedImages);

dispatch({type: actionNames.UPDATE_LOADING_IS_IN_PROGRESS, payload: false});
dispatch({type: actionNames.UPDATE_LOADING_TITLE, payload: 'All images committed!'});
dispatch(createNotification('commitScreenshot', 'success', 'Screenshots were successfully committed'));
} else {
const errorMessage = [
`Unexpected statuscode from the service: ${response.status}.`,
`Unexpected status code from the service: ${response.status}.`,
`Server response: '${response.data}'`
].join('\n');

throw new Error(errorMessage);
}
} catch (e) {
console.error('Error while comitting screenshot:', e);
console.error('An error occurred while commiting screenshot:', e);
dispatch(createNotificationError('commitScreenshot', e));

return {error: e as Error};
} finally {
dispatch({type: actionNames.UPDATE_LOADING_VISIBILITY, payload: false});
dispatch({type: actionNames.PROCESS_END});
}

return {};
};
};

type StaticAccepterUpdateToolbarPositionAction = Action<typeof actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET, {offset: Point}>;
export const staticAccepterUpdateToolbarOffset = (payload: {offset: Point}): StaticAccepterUpdateToolbarPositionAction => {
return {type: actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET, payload};
};

type StaticAccepterUpdateCommitMessageAction = Action<typeof actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE, {commitMessage: string}>;
export const staticAccepterUpdateCommitMessage = (payload: {commitMessage: string}): StaticAccepterUpdateCommitMessageAction => {
return {type: actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE, payload};
};
9 changes: 9 additions & 0 deletions lib/static/modules/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,22 @@ export default Object.assign({config: configDefaults}, {
currentNamedImageId: null
},
loading: {
taskTitle: 'Loading Testplane UI',
isVisible: true,
isInProgress: true,
progress: {}
},
staticImageAccepterModal: {
commitMessage: 'chore: update screenshot references'
}
},
ui: {
suitesPage: {
expandedSectionsById: {},
expandedStepsByResultId: {}
},
staticImageAccepterToolbar: {
offset: {x: 0, y: 0}
}
}
}) satisfies State;
3 changes: 2 additions & 1 deletion lib/static/modules/reducers/is-initialized.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import actionNames from '../action-names';
import {applyStateUpdate} from '@/static/modules/utils';

export default (state, action) => {
switch (action.type) {
case actionNames.INIT_GUI_REPORT:
case actionNames.INIT_STATIC_REPORT:
return {...state, app: {...state.app, isInitialized: true}};
return applyStateUpdate(state, {app: {isInitialized: true, loading: {isVisible: false}}});

default:
return state;
Expand Down
24 changes: 24 additions & 0 deletions lib/static/modules/reducers/loading.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,30 @@ export default (state, action) => {
});
}

case actionNames.UPDATE_LOADING_IS_IN_PROGRESS: {
return applyStateUpdate(state, {
app: {
loading: {isInProgress: action.payload}
}
});
}

case actionNames.UPDATE_LOADING_VISIBILITY: {
return applyStateUpdate(state, {
app: {
loading: {isVisible: action.payload}
}
});
}

case actionNames.UPDATE_LOADING_TITLE: {
return applyStateUpdate(state, {
app: {
loading: {taskTitle: action.payload}
}
});
}

default:
return state;
}
Expand Down
28 changes: 24 additions & 4 deletions lib/static/modules/reducers/static-image-accepter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {get, set, last, groupBy} from 'lodash';
import actionNames from '../action-names';
import {checkIsEnabled, getLocalStorageCommitedImageIds} from '../static-image-accepter';
import {applyStateUpdate, isAcceptable, isNodeSuccessful} from '../utils';
import {COMMITED, STAGED} from '../../../constants';
import {COMMITED, EditScreensFeature, STAGED} from '../../../constants';

export default (state, action) => {
switch (action.type) {
Expand All @@ -11,7 +11,7 @@ export default (state, action) => {
return state;
}

return {...state, staticImageAccepter: initStaticImageAccepter(action.payload.tree)};
return applyStateUpdate(state, {app: {availableFeatures: [EditScreensFeature]}, staticImageAccepter: initStaticImageAccepter(action.payload.tree)});
}

case actionNames.STATIC_ACCEPTER_DELAY_SCREENSHOT: {
Expand All @@ -38,7 +38,7 @@ export default (state, action) => {

for (const imageId of imageIdsToStage) {
const stateImageIds = getStateImageIds(state.tree, imageId);
const stagedImageId = stateImageIds.find(imageId => acceptableImages[imageId].commitStatus === STAGED);
const stagedImageId = stateImageIds.find(imageId => acceptableImages[imageId]?.commitStatus === STAGED);

set(acceptableImagesDiff, [imageId, 'commitStatus'], STAGED);

Expand Down Expand Up @@ -78,7 +78,7 @@ export default (state, action) => {

for (const imageId of action.payload) {
const stateImageIds = getStateImageIds(state.tree, imageId);
const commitedImageId = stateImageIds.find(imageId => acceptableImages[imageId].commitStatus === COMMITED);
const commitedImageId = stateImageIds.find(imageId => acceptableImages[imageId]?.commitStatus === COMMITED);

if (commitedImageId) {
set(acceptableImagesDiff, [commitedImageId, 'commitStatus'], null);
Expand All @@ -93,6 +93,26 @@ export default (state, action) => {
return applyStateUpdate(state, diff);
}

case actionNames.STATIC_ACCEPTER_UPDATE_TOOLBAR_OFFSET: {
return applyStateUpdate(state, {
ui: {
staticImageAccepterToolbar: {
offset: action.payload.offset
}
}
});
}

case actionNames.STATIC_ACCEPTER_UPDATE_COMMIT_MESSAGE: {
return applyStateUpdate(state, {
app: {
staticImageAccepterModal: {
commitMessage: action.payload.commitMessage
}
}
});
}

default:
return state;
}
Expand Down
18 changes: 6 additions & 12 deletions lib/static/modules/static-image-accepter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import {get} from 'lodash';
import type {ReporterConfig} from '../../types';
import {COMMITED, STAGED} from '../../constants';
import * as localStorage from './local-storage-wrapper';
import {ImageEntity, ImageEntityFail} from '@/static/new-ui/types/store';

let isEnabled: boolean | null = null;

interface AcceptableImage {
export interface AcceptableImage {
id: string;
parentId: string;
stateName: string;
Expand All @@ -15,14 +16,7 @@ interface AcceptableImage {
originalStatus: string;
}

type ImagesById = Record<string, {
id: string;
status: string;
stateName: string;
parentId: string;
actualImg: {path: string, size: {width: number, height: number}};
refImg: {path: string, relativePath?: string, size: null | {width: number, height: number}}
}>
type ImagesById = Record<string, ImageEntity>;

interface LocalStorageValue {
date: string,
Expand Down Expand Up @@ -69,16 +63,16 @@ export const formatCommitPayload = (
.map(image => ({imageId: image.id, stateNameImageId: image.stateNameImageId}))
.concat(extraImages);

if (imagesToCommit.find(({imageId}) => !imagesById[imageId].refImg.relativePath)) {
if (imagesToCommit.find(({imageId}) => !imagesById[imageId].refImg?.relativePath)) {
throw new Error(`The version of your tool does not support static image accepter: missing "relativePath"`);
}

return imagesToCommit.map(({imageId, stateNameImageId}) => ({
id: imageId,
stateNameImageId,
image: imagesById[imageId].actualImg.path,
image: (imagesById[imageId] as ImageEntityFail).actualImg.path,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
path: imagesById[imageId].refImg.relativePath!
path: (imagesById[imageId] as ImageEntityFail).refImg.relativePath!
}));
};

Expand Down
2 changes: 1 addition & 1 deletion lib/static/modules/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function isNodeSuccessful(node) {
* @returns {boolean}
*/
export function isAcceptable({status, error}) {
return isErrorStatus(status) && isNoRefImageError(error) || isFailStatus(status) || isSkippedStatus(status) || isInvalidRefImageError(error);
return isErrorStatus(status) && (isNoRefImageError(error) || isInvalidRefImageError(error)) || isFailStatus(status) || isSkippedStatus(status);
}

function isScreenGuiRevertable({gui, image, isLastResult}) {
Expand Down
45 changes: 25 additions & 20 deletions lib/static/new-ui/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {ThemeProvider} from '@gravity-ui/uikit';
import {Eye, ListCheck} from '@gravity-ui/icons';
import {ThemeProvider, ToasterComponent, ToasterProvider} from '@gravity-ui/uikit';
import '@gravity-ui/uikit/styles/fonts.css';
import '@gravity-ui/uikit/styles/styles.css';
import React, {ReactNode, StrictMode} from 'react';
import {MainLayout} from '../components/MainLayout';
import {Provider} from 'react-redux';
import {HashRouter, Navigate, Route, Routes} from 'react-router-dom';
import {Eye, ListCheck} from '@gravity-ui/icons';

import {LoadingBar} from '@/static/new-ui/components/LoadingBar';
import {GuiniToolbarOverlay} from '@/static/new-ui/components/GuiniToolbarOverlay';
import {MainLayout} from '../components/MainLayout';
import {SuitesPage} from '../features/suites/components/SuitesPage';
import {VisualChecksPage} from '../features/visual-checks/components/VisualChecksPage';

import '@gravity-ui/uikit/styles/fonts.css';
import '@gravity-ui/uikit/styles/styles.css';
import '../../new-ui.css';
import {Provider} from 'react-redux';
import store from '../../modules/store';
import {LoadingBar} from '@/static/new-ui/components/LoadingBar';

export function App(): ReactNode {
const pages = [
Expand All @@ -20,24 +21,28 @@ export function App(): ReactNode {
url: '/suites',
icon: ListCheck,
element: <SuitesPage/>,
children: [<Route key={'suite'} path=':suiteId' element= {<SuitesPage/>} />]
children: [<Route key={'suite'} path=':suiteId' element={<SuitesPage/>} />]
},
{title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: <VisualChecksPage/>}
];

return <StrictMode>
<ThemeProvider theme='light'>
<Provider store={store}>
<HashRouter>
<MainLayout menuItems={pages}>
<LoadingBar/>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
</Routes>
</MainLayout>
</HashRouter>
</Provider>
<ToasterProvider>
<Provider store={store}>
<HashRouter>
<MainLayout menuItems={pages}>
<LoadingBar/>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
</Routes>
<GuiniToolbarOverlay/>
<ToasterComponent />
</MainLayout>
</HashRouter>
</Provider>
</ToasterProvider>
</ThemeProvider>
</StrictMode>;
}
10 changes: 10 additions & 0 deletions lib/static/new-ui/components/AssertViewResult/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ function AssertViewResultInternal({result, diffMode, style}: AssertViewResultPro
<ImageLabel title={'Expected'} subtitle={getImageDisplayedSize(result.expectedImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.expectedImg} />
</div>;
} else if (result.status === TestStatus.STAGED) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Staged'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
} else if (result.status === TestStatus.COMMITED) {
return <div className={styles.screenshotContainer}>
<ImageLabel title={'Committed'} subtitle={getImageDisplayedSize(result.actualImg)} />
<Screenshot containerStyle={style} containerClassName={styles.screenshot} image={result.actualImg} />
</div>;
}

return null;
Expand Down
Loading

0 comments on commit f70e78b

Please sign in to comment.