Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add static image accepter support to new ui #612

Merged
merged 2 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}`});
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
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
Loading