From 71980b15a7860519a419b14296c0c3b3006db341 Mon Sep 17 00:00:00 2001 From: Andreas Gasser Date: Thu, 4 Apr 2019 22:01:53 +0200 Subject: [PATCH] feat(#104): added test cases for faces and labels redux subtree --- src/images/detail/Container.js | 4 +- src/redux/faces/__data__/faces.js | 211 ++++++++++++++++++++ src/redux/faces/__tests__/index.test.js | 71 +++++++ src/redux/faces/__tests__/selector.test.js | 61 ++++++ src/redux/faces/index.js | 4 + src/redux/faces/selectors.js | 2 +- src/redux/images/index.js | 3 + src/redux/labels/__data__/labels.js | 161 +++++++++++++++ src/redux/labels/__tests__/index.test.js | 71 +++++++ src/redux/labels/__tests__/selector.test.js | 61 ++++++ src/redux/labels/index.js | 4 + src/redux/labels/selectors.js | 4 +- 12 files changed, 652 insertions(+), 5 deletions(-) create mode 100644 src/redux/faces/__data__/faces.js create mode 100644 src/redux/faces/__tests__/index.test.js create mode 100644 src/redux/faces/__tests__/selector.test.js create mode 100644 src/redux/labels/__data__/labels.js create mode 100644 src/redux/labels/__tests__/index.test.js create mode 100644 src/redux/labels/__tests__/selector.test.js diff --git a/src/images/detail/Container.js b/src/images/detail/Container.js index 8ed484b..186c760 100644 --- a/src/images/detail/Container.js +++ b/src/images/detail/Container.js @@ -4,7 +4,7 @@ import { getImage } from '../../redux/images'; import { imagesByIdSelector, getImageRequestSelector } from '../../redux/images/selectors'; -import { labelsByImageId, labelsByIdSelector } from '../../redux/labels/selectors'; +import { labelsByImageIdSelector, labelsByIdSelector } from '../../redux/labels/selectors'; import { facesByImageId, facesByIdSelector } from '../../redux/faces/selectors'; import DetailView from './DetailView'; @@ -36,7 +36,7 @@ const select = (state, props) => { return { image, - labels: labelsByImageId(state, id), + labels: labelsByImageIdSelector(state, id), faces: facesByImageId(state, id), selectedFace: params.face ? facesByIdSelector(state)[params.face] || null diff --git a/src/redux/faces/__data__/faces.js b/src/redux/faces/__data__/faces.js new file mode 100644 index 0000000..2f2f06c --- /dev/null +++ b/src/redux/faces/__data__/faces.js @@ -0,0 +1,211 @@ +export default { + "faces": { + "items": [{ + "id": "7efa73a7-6fe0-404b-a85e-e34abb20b572", + "age": { + "low": 20, + "high": 38, + "__typename": "FaceAge" + }, + "position": { + "height": 0.16048859059810638, + "width": 0.06429165601730347, + "left": 0.7339414358139038, + "top": 0.25065237283706665, + "__typename": "BoundingBox" + }, + "emotions": [{ + "name": "ANGRY", + "confidence": 9.131677627563477, + "__typename": "Attribute" + }, { + "name": "SURPRISED", + "confidence": 8.68793773651123, + "__typename": "Attribute" + }, { + "name": "CALM", + "confidence": 53.679351806640625, + "__typename": "Attribute" + }, { + "name": "DISGUSTED", + "confidence": 14.890134811401367, + "__typename": "Attribute" + }, { + "name": "SAD", + "confidence": 10.600337028503418, + "__typename": "Attribute" + }, { + "name": "HAPPY", + "confidence": 0.4520353376865387, + "__typename": "Attribute" + }, { + "name": "CONFUSED", + "confidence": 0, + "__typename": "Attribute" + }], + "attributes": [{ + "name": "gender", + "confidence": 61.0933837890625, + "value": "Male", + "__typename": "Attribute" + }, { + "name": "brightness", + "confidence": 100, + "value": "83.16773986816406", + "__typename": "Attribute" + }, { + "name": "sharpness", + "confidence": 100, + "value": "92.22801208496094", + "__typename": "Attribute" + }], + "__typename": "Face" + }, { + "id": "68858073-9f89-4b6d-9526-400fdf82ed49", + "age": { + "low": 26, + "high": 43, + "__typename": "FaceAge" + }, + "position": { + "height": 0.15534932911396027, + "width": 0.06539079546928406, + "left": 0.1794459968805313, + "top": 0.2902233898639679, + "__typename": "BoundingBox" + }, + "emotions": [{ + "name": "SAD", + "confidence": 2.8847672939300537, + "__typename": "Attribute" + }, { + "name": "HAPPY", + "confidence": 0.10791733115911484, + "__typename": "Attribute" + }, { + "name": "DISGUSTED", + "confidence": 12.038032531738281, + "__typename": "Attribute" + }, { + "name": "CALM", + "confidence": 62.864418029785156, + "__typename": "Attribute" + }, { + "name": "ANGRY", + "confidence": 11.946447372436523, + "__typename": "Attribute" + }, { + "name": "CONFUSED", + "confidence": 0, + "__typename": "Attribute" + }, { + "name": "SURPRISED", + "confidence": 8.280786514282227, + "__typename": "Attribute" + }], + "attributes": [{ + "name": "gender", + "confidence": 76.8848876953125, + "value": "Male", + "__typename": "Attribute" + }, { + "name": "brightness", + "confidence": 100, + "value": "78.37348937988281", + "__typename": "Attribute" + }, { + "name": "sharpness", + "confidence": 100, + "value": "94.08262634277344", + "__typename": "Attribute" + }], + "__typename": "Face" + }, { + "id": "e686fda1-b83c-46b5-8086-81902f558e81", + "age": { + "low": 26, + "high": 43, + "__typename": "FaceAge" + }, + "position": { + "height": 0.12948490679264069, + "width": 0.0694088414311409, + "left": 0.4533727467060089, + "top": 0.33818867802619934, + "__typename": "BoundingBox" + }, + "emotions": [{ + "name": "ANGRY", + "confidence": 0.025381267070770264, + "__typename": "Attribute" + }, { + "name": "SAD", + "confidence": 0.005187096539884806, + "__typename": "Attribute" + }, { + "name": "DISGUSTED", + "confidence": 0.025570334866642952, + "__typename": "Attribute" + }, { + "name": "HAPPY", + "confidence": 99.68476867675781, + "__typename": "Attribute" + }, { + "name": "SURPRISED", + "confidence": 0.21237444877624512, + "__typename": "Attribute" + }, { + "name": "CALM", + "confidence": 0.027801742777228355, + "__typename": "Attribute" + }, { + "name": "CONFUSED", + "confidence": 0, + "__typename": "Attribute" + }], + "attributes": [{ + "name": "eyeglasses", + "confidence": 99.98456573486328, + "value": "true", + "__typename": "Attribute" + }, { + "name": "eyesOpen", + "confidence": 100, + "value": "true", + "__typename": "Attribute" + }, { + "name": "gender", + "confidence": 99.29379272460938, + "value": "Female", + "__typename": "Attribute" + }, { + "name": "mouthOpen", + "confidence": 98.84275817871094, + "value": "true", + "__typename": "Attribute" + }, { + "name": "smile", + "confidence": 99.58173370361328, + "value": "true", + "__typename": "Attribute" + }, { + "name": "sunglasses", + "confidence": 99.93656158447266, + "value": "true", + "__typename": "Attribute" + }, { + "name": "brightness", + "confidence": 100, + "value": "60.014461517333984", + "__typename": "Attribute" + }, { + "name": "sharpness", + "confidence": 100, + "value": "83.14741516113281", + "__typename": "Attribute" + }], + "__typename": "Face" + }], + "__typename": "FacePayload", + }, +}; diff --git a/src/redux/faces/__tests__/index.test.js b/src/redux/faces/__tests__/index.test.js new file mode 100644 index 0000000..4bdd237 --- /dev/null +++ b/src/redux/faces/__tests__/index.test.js @@ -0,0 +1,71 @@ +/* global it testUtils */ +import reducer, * as reduxFaces from '../index'; + +import data from '../__data__/faces'; + +const { __testables__ } = reduxFaces; + +const mockData = { + imageId: '2a2bdf23-e73f-4a2a-913e-da29926a195c', +}; + +describe('faces simple action test suite', () => { + it('should handle facesAddFaces', () => { + const { faces } = data; + const { imageId } = mockData; + + // generate output + const ids = []; + const byId = {}; + faces.items.forEach((face) => { + const { id } = face; + + ids.push(id); + byId[id] = face; + }); + + // test function + expect(reduxFaces.facesAddFaces(imageId, faces.items)) + .toEqual({ + type: __testables__.FACES_ADD_FACES, + payload: { + imageId, + ids, + byId, + }, + }); + }); +}); + +describe('faces reducer test suite', () => { + it('should return initial state', () => { + expect(reducer(undefined, testUtils.dummyTestAction())) + .toEqual({ + byId: {}, + idsByImageId: {}, + }); + }); + + it('should handle FACES_ADD_FACES', () => { + const { faces } = data; + const { imageId } = mockData; + + // generate output + const ids = []; + const byId = {}; + faces.items.forEach((face) => { + const { id } = face; + + ids.push(id); + byId[id] = face; + }); + + expect(reducer(undefined, reduxFaces.facesAddFaces(imageId, faces.items))) + .toEqual({ + byId, + idsByImageId: { + [imageId]: ids, + }, + }); + }); +}); \ No newline at end of file diff --git a/src/redux/faces/__tests__/selector.test.js b/src/redux/faces/__tests__/selector.test.js new file mode 100644 index 0000000..9742ab3 --- /dev/null +++ b/src/redux/faces/__tests__/selector.test.js @@ -0,0 +1,61 @@ +/* global testUtils */ +import * as selectors from '../selectors'; + +import data from '../__data__/faces'; +const { faces } = data; + +const imageId = '2a2bdf23-e73f-4a2a-913e-da29926a195c'; + +const initialState = { + faces: { + byId: faces.items.reduce((prev, cur) => ({ + ...prev, + [cur.id]: cur, + }), {}), + idsByImageId: { + [imageId]: faces.items.map(item => item.id), + }, + imageId, + }, +}; + +describe('faces selector test suite', () => { + it('should return facesState state', () => { + expect(selectors.facesStateSelector(initialState)) + .toEqual(initialState.faces); + }); + + it('should return facesIdsByImageId value', () => { + expect(selectors.facesIdsByImageIdSelector(initialState)) + .toEqual(initialState.faces.idsByImageId); + }); + + it('should return facesById value', () => { + expect(selectors.facesByIdSelector(initialState)) + .toEqual(initialState.faces.byId); + }); + + it('should return facesByImageId value', () => { + const idsByImageId = selectors.facesIdsByImageIdSelector(initialState); + const byId = selectors.facesByIdSelector(initialState); + + const ids = idsByImageId[imageId]; + + const faces = ids.map(id => byId[id]) + .filter(face => face !== undefined); + + // const labels; + expect(selectors.facesByImageId(initialState, imageId)) + .toEqual(faces); + }); + + it('should return empty array for facesByImageId without imageId', () => { + expect(selectors.facesByImageId(initialState, undefined)) + .toEqual([]); + }); + + it('should return empty array for facesByImageId with wrong imageId', () => { + expect(selectors.facesByImageId(initialState, 'invalid')) + .toEqual([]); + }); +}); diff --git a/src/redux/faces/index.js b/src/redux/faces/index.js index 6ac3607..3d99d1e 100644 --- a/src/redux/faces/index.js +++ b/src/redux/faces/index.js @@ -63,6 +63,10 @@ const persistConfig = { stateReconciler: autoMergeLevel2, }; +export const __testables__ = { + FACES_ADD_FACES, +}; + export default persistReducer( persistConfig, combineReducers({ diff --git a/src/redux/faces/selectors.js b/src/redux/faces/selectors.js index ba0a120..5810098 100644 --- a/src/redux/faces/selectors.js +++ b/src/redux/faces/selectors.js @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; -const facesStateSelector = state => state.faces; +export const facesStateSelector = state => state.faces; export const facesIdsByImageIdSelector = createSelector( facesStateSelector, diff --git a/src/redux/images/index.js b/src/redux/images/index.js index adf6bb9..cd299b9 100644 --- a/src/redux/images/index.js +++ b/src/redux/images/index.js @@ -283,6 +283,9 @@ export const getImage = hocAsyncAction( const { faces, labels, ...image } = getImage; const { id } = image; + + console.log(faces); + console.log(labels); dispatch(imagesAddImage(image)); dispatch(labelsAddLabels(id, labels.items)); diff --git a/src/redux/labels/__data__/labels.js b/src/redux/labels/__data__/labels.js new file mode 100644 index 0000000..005dfa5 --- /dev/null +++ b/src/redux/labels/__data__/labels.js @@ -0,0 +1,161 @@ +export default { + "labels": { + "items": [{ + "id": "a465d79f-f65c-44bd-9974-7d8ecb0325c7", + "name": "Restaurant", + "confidence": 99.84324645996094, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "8bfbfb55-d4ac-46fc-8d09-3bc98a5040bb", + "name": "Human", + "confidence": 99.75186920166016, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "f705f130-40ed-4c66-95e3-7d63e438ed5a", + "name": "Person", + "confidence": 99.75186920166016, + "parents": [], + "instances": [{ + "height": 0.788449764251709, + "width": 0.37145188450813293, + "left": 0.5232441425323486, + "top": 0.20031282305717468, + "__typename": "BoundingBox" + }, { + "height": 0.27881088852882385, + "width": 0.11530932039022446, + "left": 0.3354623317718506, + "top": 0.1961989402770996, + "__typename": "BoundingBox" + }, { + "height": 0.7772222757339478, + "width": 0.352791428565979, + "left": 0.03594960644841194, + "top": 0.20243647694587708, + "__typename": "BoundingBox" + }, { + "height": 0.3490219712257385, + "width": 0.3184428811073303, + "left": 0.3262154459953308, + "top": 0.2951163053512573, + "__typename": "BoundingBox" + }], + "__typename": "Label" + }, { + "id": "aec89fb8-7840-4462-9658-62a6bf54599f", + "name": "Sitting", + "confidence": 99.7125015258789, + "parents": ["Person"], + "instances": [], + "__typename": "Label" + }, { + "id": "30bd00f0-8a13-4f5a-9a5c-8408182bb75d", + "name": "Furniture", + "confidence": 98.73352813720703, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "65b4c64e-0737-4b7a-be84-77c7577ceaca", + "name": "Chair", + "confidence": 98.73352813720703, + "parents": ["Furniture"], + "instances": [{ + "height": 0.35435470938682556, + "width": 0.30652856826782227, + "left": 0.012152209877967834, + "top": 0.6453336477279663, + "__typename": "BoundingBox" + }], + "__typename": "Label" + }, { + "id": "0ac2318e-67c3-4ddc-adc2-0711bb4766fe", + "name": "Sunglasses", + "confidence": 97.88621520996094, + "parents": ["Accessories"], + "instances": [{ + "height": 0.0482422336935997, + "width": 0.07262253016233444, + "left": 0.4590190351009369, + "top": 0.3799567222595215, + "__typename": "BoundingBox" + }], + "__typename": "Label" + }, { + "id": "71ab816d-8dc3-403a-9a5c-bff2bd7f73b5", + "name": "Accessory", + "confidence": 97.88621520996094, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "d8436c1a-9d42-4070-9623-caa44e3bd2b6", + "name": "Accessories", + "confidence": 97.88621520996094, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "86af5c75-9c6f-435f-8cb0-d4ae73e26f4c", + "name": "Cafe", + "confidence": 95.34770965576172, + "parents": ["Restaurant"], + "instances": [], + "__typename": "Label" + }, { + "id": "6b934aa7-35fb-4f32-a69b-2d5a067db392", + "name": "Food Court", + "confidence": 94.11480712890625, + "parents": ["Restaurant", "Food"], + "instances": [], + "__typename": "Label" + }, { + "id": "ad9d4ff2-5961-442e-a985-3af9a71dd1c3", + "name": "Food", + "confidence": 94.11480712890625, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "c53fed94-b6b5-44cc-90c4-2d221051c895", + "name": "Cafeteria", + "confidence": 92.61895751953125, + "parents": ["Restaurant"], + "instances": [], + "__typename": "Label" + }, { + "id": "4deaecd3-f04e-4527-9665-63bb2c9fb6be", + "name": "Dating", + "confidence": 82.52543640136719, + "parents": ["Person"], + "instances": [], + "__typename": "Label" + }, { + "id": "0aaf6e3f-fb3a-4853-9746-31179fe464ab", + "name": "Electronics", + "confidence": 58.003883361816406, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "cb2b4155-f2fd-406e-befa-cf2e5126ee0d", + "name": "Hair", + "confidence": 51.47688293457031, + "parents": [], + "instances": [], + "__typename": "Label" + }, { + "id": "54d71713-637c-4124-a4de-42b8b176ac58", + "name": "Couch", + "confidence": 50.86988830566406, + "parents": ["Furniture"], + "instances": [], + "__typename": "Label" + }], + "__typename": "LabelPayload" + }, +}; diff --git a/src/redux/labels/__tests__/index.test.js b/src/redux/labels/__tests__/index.test.js new file mode 100644 index 0000000..ef31393 --- /dev/null +++ b/src/redux/labels/__tests__/index.test.js @@ -0,0 +1,71 @@ +/* global it testUtils */ +import reducer, * as reduxLabels from '../index'; + +import data from '../__data__/labels'; + +const { __testables__ } = reduxLabels; + +const mockData = { + imageId: '2a2bdf23-e73f-4a2a-913e-da29926a195c', +}; + +describe('labels simple action test suite', () => { + it('should handle labelsAddLabels', () => { + const { labels } = data; + const { imageId } = mockData; + + // generate output + const ids = []; + const byId = {}; + labels.items.forEach((face) => { + const { id } = face; + + ids.push(id); + byId[id] = face; + }); + + // test function + expect(reduxLabels.labelsAddLabels(imageId, labels.items)) + .toEqual({ + type: __testables__.LABELS_ADD_LABELS, + payload: { + imageId, + ids, + byId, + }, + }); + }); +}); + +describe('labels reducer test suite', () => { + it('should return initial state', () => { + expect(reducer(undefined, testUtils.dummyTestAction())) + .toEqual({ + byId: {}, + idsByImageId: {}, + }); + }); + + it('should handle FACES_ADD_FACES', () => { + const { labels } = data; + const { imageId } = mockData; + + // generate output + const ids = []; + const byId = {}; + labels.items.forEach((label) => { + const { id } = label; + + ids.push(id); + byId[id] = label; + }); + + expect(reducer(undefined, reduxLabels.labelsAddLabels(imageId, labels.items))) + .toEqual({ + byId, + idsByImageId: { + [imageId]: ids, + }, + }); + }); +}); \ No newline at end of file diff --git a/src/redux/labels/__tests__/selector.test.js b/src/redux/labels/__tests__/selector.test.js new file mode 100644 index 0000000..4297b89 --- /dev/null +++ b/src/redux/labels/__tests__/selector.test.js @@ -0,0 +1,61 @@ +/* global testUtils */ +import * as selectors from '../selectors'; + +import data from '../__data__/labels'; +const { labels } = data; + +const imageId = '2a2bdf23-e73f-4a2a-913e-da29926a195c'; + +const initialState = { + labels: { + byId: labels.items.reduce((prev, cur) => ({ + ...prev, + [cur.id]: cur, + }), {}), + idsByImageId: { + [imageId]: labels.items.map(item => item.id), + }, + imageId, + }, +}; + +describe('labels selector test suite', () => { + it('should return labelsState state', () => { + expect(selectors.labelsStateSelector(initialState)) + .toEqual(initialState.labels); + }); + + it('should return labelsIdsByImageId value', () => { + expect(selectors.labelsIdsByImageIdSelector(initialState)) + .toEqual(initialState.labels.idsByImageId); + }); + + it('should return labelsById value', () => { + expect(selectors.labelsByIdSelector(initialState)) + .toEqual(initialState.labels.byId); + }); + + it('should return labelsByImageId value', () => { + const idsByImageId = selectors.labelsIdsByImageIdSelector(initialState); + const byId = selectors.labelsByIdSelector(initialState); + + const ids = idsByImageId[imageId]; + + const labels = ids.map(id => byId[id]) + .filter(label => label !== undefined); + + // const labels; + expect(selectors.labelsByImageIdSelector(initialState, imageId)) + .toEqual(labels); + }); + + it('should return empty array for labelsByImageId without imageId', () => { + expect(selectors.labelsByImageIdSelector(initialState, undefined)) + .toEqual([]); + }); + + it('should return empty array for labelsByImageId with wrong imageId', () => { + expect(selectors.labelsByImageIdSelector(initialState, 'invalid')) + .toEqual([]); + }); +}); diff --git a/src/redux/labels/index.js b/src/redux/labels/index.js index d1153f1..c840912 100644 --- a/src/redux/labels/index.js +++ b/src/redux/labels/index.js @@ -63,6 +63,10 @@ const persistConfig = { stateReconciler: autoMergeLevel2, }; +export const __testables__ = { + LABELS_ADD_LABELS, +}; + export default persistReducer( persistConfig, combineReducers({ diff --git a/src/redux/labels/selectors.js b/src/redux/labels/selectors.js index 70a38d6..ca0b3dc 100644 --- a/src/redux/labels/selectors.js +++ b/src/redux/labels/selectors.js @@ -1,6 +1,6 @@ import { createSelector } from 'reselect'; -const labelsStateSelector = state => state.labels; +export const labelsStateSelector = state => state.labels; export const labelsIdsByImageIdSelector = createSelector( labelsStateSelector, @@ -13,7 +13,7 @@ export const labelsByIdSelector = createSelector( ); // dynamic selectors -export const labelsByImageId = (state, imageId) => { +export const labelsByImageIdSelector = (state, imageId) => { if (!imageId) { return []; }