SDA-2533 Add clear, erase and done functionality. Further optimizations of logic.

This commit is contained in:
psjostrom 2020-11-19 09:17:39 +01:00
parent 6811683e29
commit 064b9b53b2
9 changed files with 257 additions and 118 deletions

View File

@ -4,7 +4,7 @@ exports[`<AnnotateArea/> should render correctly 1`] = `
<svg
data-testid="annotate-area"
height={800}
id="annotate"
id="annotate-area"
onMouseDown={[Function]}
onMouseLeave={[Function]}
onMouseMove={[Function]}

View File

@ -57,25 +57,28 @@ exports[`Snipping Tool should render correctly 1`] = `
</button>
</div>
</header>
<main>
<div
className="imageContainer"
>
<AnnotateArea
chosenTool="PEN"
highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={
Object {
"height": 600,
"width": 800,
}
<main
style={
Object {
"minHeight": 200,
}
}
>
<AnnotateArea
chosenTool="PEN"
data-testid="annotate-component"
highlightColor="rgba(0, 142, 255, 0.64)"
imageDimensions={
Object {
"height": 600,
"width": 800,
}
onChange={[Function]}
paths={Array []}
penColor="rgba(0, 142, 255, 1)"
screenSnippetPath="Screen-Snippet"
/>
</div>
}
onChange={[Function]}
paths={Array []}
penColor="rgba(0, 142, 255, 1)"
screenSnippetPath="Screen-Snippet"
/>
</main>
<footer>
<button

View File

@ -37,7 +37,7 @@ describe('<AnnotateArea/>', () => {
expect(defaultProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(38, 196, 58, 1)',
key: 'path0',
points: [{ x: 0, y: 0 }],
points: [{ x: 2, y: 1 }],
shouldShow: true,
strokeWidth: 5,
}]);
@ -89,4 +89,32 @@ describe('<AnnotateArea/>', () => {
const wrapper = shallow(<AnnotateArea {...defaultProps} />);
expect(wrapper.find('[data-testid="path0"]').exists()).toEqual(false);
});
it('should call onChange with hidden path if clicked on path with tool eraser', () => {
const pathProps = {
paths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
highlightColor: 'rgba(233, 0, 0, 0.64)',
penColor: 'rgba(38, 196, 58, 1)',
onChange: jest.fn(),
imageDimensions: { width: 800, height: 800 },
chosenTool: Tool.eraser,
screenSnippetPath: 'very-nice-path',
};
const wrapper = shallow(<AnnotateArea {...pathProps} />);
const path = wrapper.find('[data-testid="path0"]');
path.simulate('click');
expect(pathProps.onChange).toHaveBeenCalledWith([{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
});
});

View File

@ -1,32 +0,0 @@
import { shallow } from 'enzyme';
import * as React from 'react';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
describe('Snipping Tool', () => {
const snippingToolLabel = 'snipping-tool-data';
const onLabelEvent = 'on';
it('should render correctly', () => {
const wrapper = shallow(React.createElement(SnippingTool));
expect(wrapper).toMatchSnapshot();
});
it('should call `snipping-tool-data` event when component is mounted', () => {
const spy = jest.spyOn(ipcRenderer, onLabelEvent);
shallow(React.createElement(SnippingTool));
expect(spy).toBeCalledWith(snippingToolLabel, expect.any(Function));
});
it('should render pen color picker when clicked on pen', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="pen-button"]').simulate('click');
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
});
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
});
});

View File

@ -0,0 +1,67 @@
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import SnippingTool from '../src/renderer/components/snipping-tool';
import { ipcRenderer } from './__mocks__/electron';
const waitForPromisesToResolve = () => new Promise((resolve) => setTimeout(resolve));
describe('Snipping Tool', () => {
it('should render correctly', () => {
const wrapper = shallow(React.createElement(SnippingTool));
expect(wrapper).toMatchSnapshot();
});
it('should set up a "once" listener for snipping-tool-data event on mounting', () => {
const spy = jest.spyOn(ipcRenderer, 'once');
shallow(React.createElement(SnippingTool));
expect(spy).toBeCalledWith('snipping-tool-data', expect.any(Function));
});
it('should render pen color picker when clicked on pen', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="pen-button"]').simulate('click');
expect(wrapper.find('[data-testid="pen-colorpicker"]').exists()).toBe(true);
});
it('should render highlight color picker when clicked on highlight', () => {
const wrapper = shallow(React.createElement(SnippingTool));
wrapper.find('[data-testid="highlight-button"]').simulate('click');
expect(wrapper.find('[data-testid="highlight-colorpicker"]').exists()).toBe(true);
});
it('should clear all paths when clicked on clear', () => {
const props = {
existingPaths: [{
points: [{ x: 0, y: 0 }],
shouldShow: true,
strokeWidth: 5,
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
}],
};
const wrapper = mount(<SnippingTool {...props} />);
const annotateComponent = wrapper.find('[data-testid="annotate-component"]');
wrapper.find('[data-testid="clear-button"]').simulate('click');
wrapper.update();
expect(annotateComponent.prop('paths')).toEqual(
[{
color: 'rgba(233, 0, 0, 0.64)',
key: 'path0',
points: [{ x: 0, y: 0 }],
shouldShow: false,
strokeWidth: 5,
}]);
});
it('should send upload-snippet event with correct data when clicked on done', async () => {
const spy = jest.spyOn(ipcRenderer, 'send');
const wrapper = mount(<SnippingTool />);
wrapper.find('[data-testid="done-button"]').simulate('click');
wrapper.update();
await waitForPromisesToResolve();
expect(spy).toBeCalledWith('upload-snippet', {
base64PngData: 'NO CANVAS',
screenSnippetPath: 'Screen-Snippet',
});
});
});

View File

@ -230,20 +230,28 @@ class ScreenSnippet {
}
// remove tmp file (async)
if (this.outputFileName) {
fs.unlink(this.outputFileName, (removeErr) => {
logger.info(
`screen-snippet-handler: cleaning up temp snippet file: ${this.outputFileName}!`,
);
if (removeErr) {
logger.error(
`screen-snippet-handler: error removing temp snippet file: ${this.outputFileName}, err: ${removeErr}`,
);
}
});
this.deleteFile(this.outputFileName);
}
}
}
/**
* Deletes a locally stored file
* @param filePath Path for the file to delete
*/
private deleteFile(filePath: string) {
fs.unlink(filePath, (removeErr) => {
logger.info(
`screen-snippet-handler: cleaning up temp snippet file: ${filePath}!`,
);
if (removeErr) {
logger.error(
`screen-snippet-handler: error removing temp snippet file: ${filePath}, err: ${removeErr}`,
);
}
});
}
/**
* Verify and updates always on top
*/
@ -279,20 +287,19 @@ class ScreenSnippet {
* @param webContents A browser window's web contents object
*/
private uploadSnippet(webContents: Electron.webContents) {
ipcMain.once('upload-snippet', async (_event, snipImage: string) => {
ipcMain.once('upload-snippet', async (_event, snippetData: { screenSnippetPath: string, base64PngData: string }) => {
windowHandler.closeSnippingToolWindow();
if (snipImage) {
this.outputFileName = snipImage;
}
const {
message,
const [type, data] = snippetData.base64PngData.split(',');
const payload = {
message: 'SUCCESS',
data,
type,
}: IScreenSnippet = await this.convertFileToData();
};
this.deleteFile(snippetData.screenSnippetPath);
logger.info(
`screen-snippet-handler: Snippet captured! Sending data to SFE`,
);
webContents.send('screen-snippet-data', { message, data, type });
webContents.send('screen-snippet-data', payload);
await this.verifyAndUpdateAlwaysOnTop();
});
}

View File

@ -33,12 +33,28 @@ const MIN_ANNOTATE_AREA_WIDTH = 312;
const PEN_WIDTH = 5;
const HIGHLIGHT_WIDTH = 28;
const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, highlightColor, penColor, onChange, imageDimensions, chosenTool, screenSnippetPath }) => {
const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({
paths,
highlightColor,
penColor,
onChange,
imageDimensions,
chosenTool,
screenSnippetPath,
}) => {
const [isDrawing, setIsDrawing] = useState(false);
const maybeErasePath = (key: string) => {
// erase logic here
return key;
if (chosenTool === Tool.eraser) {
const updPaths = [...paths];
updPaths.map((p) => {
if (p && p.key === key) {
p.shouldShow = false;
}
return p;
});
onChange(updPaths);
}
};
const stopDrawing = () => {
@ -59,10 +75,12 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
// Render and preparing render functions
const addHighlightPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1];
const shouldShow = true;
const key = 'path' + paths.length;
if (!isDrawing) {
if (isDrawing) {
const activePath = paths[paths.length - 1];
activePath.points.push(point);
} else {
const shouldShow = true;
const key = 'path' + paths.length;
paths.push({
points: [point],
color: highlightColor,
@ -70,17 +88,17 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
shouldShow,
key,
});
} else {
activePath.points.push(point);
}
return paths;
};
const addPenPoint = (paths: IPath[], point: IPoint) => {
const activePath = paths[paths.length - 1];
const shouldShow = true;
const key = 'path' + paths.length;
if (!isDrawing) {
if (isDrawing) {
const activePath = paths[paths.length - 1];
activePath.points.push(point);
} else {
const shouldShow = true;
const key = 'path' + paths.length;
paths.push({
points: [point],
color: penColor,
@ -88,21 +106,19 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
shouldShow,
key,
});
} else {
activePath.points.push(point);
}
return paths;
};
const addPathPoint = (e: React.MouseEvent) => {
const p = [...paths];
const mousePos = getMousePosition(e);
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
const mousePos: IPoint = getMousePosition(e);
if (chosenTool === Tool.highlight) {
lazy.update({ x: mousePos.x, y: mousePos.y });
const point: IPoint = lazy.getBrushCoordinates();
onChange(addHighlightPoint(p, point));
} else {
onChange(addPenPoint(p, point));
onChange(addPenPoint(p, mousePos));
}
if (!isDrawing) {
setIsDrawing(true);
@ -204,7 +220,7 @@ const AnnotateArea: React.FunctionComponent<IAnnotateAreaProps> = ({ paths, high
<svg
data-testid='annotate-area'
style={{ cursor: 'crosshair' }}
id='annotate'
id='annotate-area'
width={imageDimensions.width}
height={imageDimensions.height}
onMouseDown={handleMouseDown}

View File

@ -2,7 +2,7 @@ import * as React from 'react';
export interface IColorPickerPillProps {
availableColors: IColor[];
onChange: (color: string) => void;
onChange: (color: IColor) => void;
}
export interface IColor {
@ -32,7 +32,7 @@ const ColorPickerPill = (props: IColorPickerPillProps) => {
const widthAndHeight = getWidthAndHeight();
const chooseColor = () => {
props.onChange(color.rgbaColor);
props.onChange(color);
};
return (

View File

@ -38,12 +38,16 @@ export interface IImageDimensions {
height: number;
}
export interface ISnippingToolProps {
existingPaths?: IPath[];
}
const availablePenColors: IColor[] = [
{ rgbaColor: 'rgba(0, 0, 40, 1)' },
{ rgbaColor: 'rgba(0, 142, 255, 1)' },
{ rgbaColor: 'rgba(38, 196, 58, 1)' },
{ rgbaColor: 'rgba(246, 178, 2, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(0, 0, 0, 1)' },
{ rgbaColor: 'rgba(255, 255, 255, 1)', outline: 'rgba(207, 208, 210, 1)' },
];
const availableHighlightColors: IColor[] = [
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
@ -53,7 +57,7 @@ const availableHighlightColors: IColor[] = [
];
const SNIPPING_TOOL_NAMESPACE = 'ScreenSnippet';
const SnippingTool = () => {
const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({ existingPaths }) => {
// State preparation functions
const [screenSnippetPath, setScreenSnippetPath] = useState('Screen-Snippet');
@ -61,11 +65,11 @@ const SnippingTool = () => {
height: 600,
width: 800,
});
const [paths, setPaths] = useState<IPath[]>([]);
const [paths, setPaths] = useState<IPath[]>(existingPaths || []);
const [chosenTool, setChosenTool] = useState(Tool.pen);
const [penColor, setPenColor] = useState('rgba(0, 142, 255, 1)');
const [highlightColor, setHighlightColor] = useState(
'rgba(0, 142, 255, 0.64)',
const [penColor, setPenColor] = useState<IColor>({ rgbaColor: 'rgba(0, 142, 255, 1)' });
const [highlightColor, setHighlightColor] = useState<IColor>(
{ rgbaColor: 'rgba(0, 142, 255, 0.64)' },
);
const [
shouldRenderHighlightColorPicker,
@ -80,15 +84,9 @@ const SnippingTool = () => {
setImageDimensions({ height, width });
};
ipcRenderer.on('snipping-tool-data', getSnipImageData);
ipcRenderer.once('snipping-tool-data', getSnipImageData);
useEffect(() => {
return () => {
ipcRenderer.removeListener('snipping-tool-data', getSnipImageData);
};
}, []);
// Hook that alerts clicks outside of the passed ref
// Hook that alerts clicks outside of the passed refs
const useClickOutsideExaminer = (
colorPickerRf: React.RefObject<HTMLDivElement>,
penRf: React.RefObject<HTMLButtonElement>,
@ -120,12 +118,12 @@ const SnippingTool = () => {
// State mutating functions
const penColorChosen = (color: string) => {
const penColorChosen = (color: IColor) => {
setPenColor(color);
setShouldRenderPenColorPicker(false);
};
const highlightColorChosen = (color: string) => {
const highlightColorChosen = (color: IColor) => {
setHighlightColor(color);
setShouldRenderHighlightColorPicker(false);
};
@ -147,11 +145,58 @@ const SnippingTool = () => {
};
const clear = () => {
// Clear logic here
const updPaths = [...paths];
updPaths.map((p) => {
p.shouldShow = false;
return p;
});
setPaths(updPaths);
};
// Utility functions
const getBase64PngData = () => {
const canvas = document.createElement('canvas');
canvas.width = imageDimensions.width;
canvas.height = imageDimensions.height;
// Creates an in memory canvas for mounting img data without adding it to the DOM
const ctx = canvas?.getContext('2d') as CanvasRenderingContext2D;
if (!ctx) {
// Will only be the case in headless browsers, such as with unit tests
return 'NO CANVAS';
}
// Creates an in memory img without adding it to the DOM
const img = document.createElement('img');
const svg = document.getElementById('annotate-area') as HTMLImageElement;
// Parses SVG image to XML data
const svgData = new XMLSerializer().serializeToString(svg);
// Adds the extracted XML data to the in memory img
img.setAttribute(
'src',
'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData))),
);
const screenSnippet = document.getElementById('screenSnippet') as HTMLImageElement;
return new Promise((resolve, reject) => {
// Listens to when the img is loaded in memory and adds the data from the SVG paths + screenSnippet to the canvas
img.onload = () => {
ctx.drawImage(screenSnippet, 0, 0);
ctx.drawImage(img, 0, 0);
try {
// Extracts base 64 png img data from the canvas
const data = canvas.toDataURL('image/png');
resolve(data);
} catch (e) {
reject(e);
}
};
});
};
const markChosenColor = (colors: IColor[], chosenColor: string) => {
return colors.map((color) => {
if (color.rgbaColor === chosenColor) {
@ -167,17 +212,21 @@ const SnippingTool = () => {
return undefined;
}
if (chosenTool === Tool.pen) {
return { border: '2px solid ' + penColor };
const color = penColor.outline ? penColor.outline : penColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.highlight) {
return { border: '2px solid ' + highlightColor };
const color = highlightColor.outline ? highlightColor.outline : highlightColor.rgbaColor;
return { border: '2px solid ' + color };
} else if (chosenTool === Tool.eraser) {
return { border: '2px solid #008EFF' };
}
return undefined;
};
const done = () => {
ipcRenderer.send('upload-snippet', screenSnippetPath);
const done = async () => {
const base64PngData = await getBase64PngData();
// const base64PngData = 'await getBase64PngData();';
ipcRenderer.send('upload-snippet', { screenSnippetPath, base64PngData });
};
return (
@ -229,7 +278,7 @@ const SnippingTool = () => {
<div style={{ position: 'relative', left: '-50%' }}>
<ColorPickerPill
data-testid='pen-colorpicker'
availableColors={markChosenColor(availablePenColors, penColor)}
availableColors={markChosenColor(availablePenColors, penColor.rgbaColor)}
onChange={penColorChosen}
/>
</div>
@ -244,7 +293,7 @@ const SnippingTool = () => {
data-testid='highlight-colorpicker'
availableColors={markChosenColor(
availableHighlightColors,
highlightColor,
highlightColor.rgbaColor,
)}
onChange={highlightColorChosen}
/>
@ -256,9 +305,10 @@ const SnippingTool = () => {
<main>
<div className='imageContainer'>
<AnnotateArea
paths={paths}
highlightColor={highlightColor}
penColor={penColor}
data-testid='annotate-component'
paths={paths}
highlightColor={highlightColor.rgbaColor}
penColor={penColor.rgbaColor}
onChange={setPaths}
imageDimensions={imageDimensions}
screenSnippetPath={screenSnippetPath}