Refactor + more tests

This commit is contained in:
viktorstrate
2021-05-19 18:21:32 +02:00
parent 3c0690c546
commit 287b0cc204
5 changed files with 452 additions and 74 deletions

View File

@@ -0,0 +1,40 @@
import '@testing-library/jest-dom'
import React from 'react'
import { render, screen } from '@testing-library/react'
import FaceCircleImage from './FaceCircleImage'
import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces'
require('../../localization').setupLocalization()
test('face circle image', () => {
const imageFace: myFaces_myFaceGroups_imageFaces = {
__typename: 'ImageFace',
id: '3',
media: {
id: '1',
__typename: 'Media',
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_my_image_jpg_p9x8dLWr.jpg',
width: 1024,
height: 641,
},
},
rectangle: {
__typename: 'FaceRectangle',
minX: 0.27,
maxX: 0.34,
minY: 0.76,
maxY: 0.88,
},
}
render(<FaceCircleImage imageFace={imageFace} selectable={true} />)
expect(screen.getByRole('img')).toBeInTheDocument()
expect(screen.getByRole('img')).toHaveAttribute(
'src',
imageFace.media.thumbnail!.url
)
})

View File

@@ -0,0 +1,115 @@
import React from 'react'
import styled from 'styled-components'
import { ProtectedImage } from '../../components/photoGallery/ProtectedMedia'
import { myFaces_myFaceGroups_imageFaces } from './__generated__/myFaces'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const FaceImage = styled(({ origin, selectable, scale, ...rest }) => (
<ProtectedImage {...rest} />
))<{ origin: { x: number; y: number }; selectable: boolean; scale: number }>`
position: absolute;
transform-origin: ${({ origin }) => `${origin.x * 100}% ${origin.y * 100}%`};
object-fit: cover;
transition: transform 250ms ease-out;
`
const FaceImagePortrait = styled(FaceImage)`
width: 100%;
top: 50%;
transform: translateY(-50%)
${({ origin, scale }) =>
`translate(${(0.5 - origin.x) * 100}%, ${
(0.5 - origin.y) * 100
}%) scale(${Math.max(scale * 0.8, 1)})`};
${({ selectable, origin, scale }) =>
selectable
? `
&:hover {
transform: translateY(-50%) translate(${(0.5 - origin.x) * 100}%, ${
(0.5 - origin.y) * 100
}%) scale(${Math.max(scale * 0.85, 1)})
`
: ''}
`
const FaceImageLandscape = styled(FaceImage)`
height: 100%;
left: 50%;
transform: translateX(-50%)
${({ origin, scale }) =>
`translate(${(0.5 - origin.x) * 100}%, ${
(0.5 - origin.y) * 100
}%) scale(${Math.max(scale * 0.8, 1)})`};
${({ selectable, origin, scale }) =>
selectable
? `
&:hover {
transform: translateX(-50%) translate(${(0.5 - origin.x) * 100}%, ${
(0.5 - origin.y) * 100
}%) scale(${Math.max(scale * 0.85, 1)})
`
: ''}
`
const CircleImageWrapper = styled.div<{ size: string }>`
background-color: #eee;
position: relative;
border-radius: 50%;
width: ${({ size }) => size};
height: ${({ size }) => size};
object-fit: fill;
overflow: hidden;
`
type FaceCircleImageProps = {
imageFace: myFaces_myFaceGroups_imageFaces
selectable: boolean
size?: string
}
const FaceCircleImage = ({
imageFace,
selectable,
size = '150px',
}: FaceCircleImageProps) => {
if (!imageFace) {
return null
}
const rect = imageFace.rectangle
const scale = Math.min(
1 / (rect.maxX - rect.minX),
1 / (rect.maxY - rect.minY)
)
const origin = {
x: (rect.minX + rect.maxX) / 2,
y: (rect.minY + rect.maxY) / 2,
}
let SpecificFaceImage: typeof FaceImageLandscape | typeof FaceImagePortrait =
FaceImageLandscape
if (imageFace.media.thumbnail) {
SpecificFaceImage =
imageFace.media.thumbnail.width > imageFace.media.thumbnail.height
? FaceImageLandscape
: FaceImagePortrait
}
return (
<CircleImageWrapper size={size}>
<SpecificFaceImage
selectable={selectable}
scale={scale}
origin={origin}
src={imageFace.media.thumbnail?.url}
/>
</CircleImageWrapper>
)
}
export default FaceCircleImage

View File

@@ -1,10 +1,15 @@
import '@testing-library/jest-dom'
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import PeoplePage, { MY_FACES_QUERY } from './PeoplePage'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import PeoplePage, {
FaceDetails,
MY_FACES_QUERY,
SET_GROUP_LABEL_MUTATION,
} from './PeoplePage'
import { MockedProvider } from '@apollo/client/testing'
import { MemoryRouter } from 'react-router'
import { myFaces_myFaceGroups } from './__generated__/myFaces'
require('../../localization').setupLocalization()
@@ -15,88 +20,202 @@ jest.mock('../../hooks/useScrollPagination', () =>
}))
)
const graphqlMocks = [
{
request: {
query: MY_FACES_QUERY,
variables: {
limit: 50,
offset: 0,
describe('PeoplePage component', () => {
const graphqlMocks = [
{
request: {
query: MY_FACES_QUERY,
variables: {
limit: 50,
offset: 0,
},
},
},
result: {
data: {
myFaceGroups: [
{
__typename: 'FaceGroup',
id: '3',
label: 'Person A',
imageFaceCount: 2,
imageFaces: [
{
__typename: 'ImageFace',
id: '3',
rectangle: {
__typename: 'FaceRectangle',
minX: 0.2705079913139343,
maxX: 0.3408200144767761,
minY: 0.7691109776496887,
maxY: 0.881434977054596,
},
media: {
__typename: 'Media',
id: '63',
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_L%C3%B8berute_jpg_p9x8dLWr.jpg',
width: 1024,
height: 641,
result: {
data: {
myFaceGroups: [
{
__typename: 'FaceGroup',
id: '3',
label: 'Person A',
imageFaceCount: 2,
imageFaces: [
{
__typename: 'ImageFace',
id: '3',
rectangle: {
__typename: 'FaceRectangle',
minX: 0.2705079913139343,
maxX: 0.3408200144767761,
minY: 0.7691109776496887,
maxY: 0.881434977054596,
},
media: {
__typename: 'Media',
id: '63',
thumbnail: {
__typename: 'MediaURL',
url: 'http://localhost:4001/photo/thumbnail_L%C3%B8berute_jpg_p9x8dLWr.jpg',
width: 1024,
height: 641,
},
},
},
},
],
},
{
__typename: 'FaceGroup',
id: '1',
label: 'Person B',
imageFaceCount: 1,
imageFaces: [],
},
],
],
},
{
__typename: 'FaceGroup',
id: '1',
label: 'Person B',
imageFaceCount: 1,
imageFaces: [],
},
],
},
},
},
},
]
]
test('people page', async () => {
const matchMock = {
params: {
person: undefined,
},
test('people page', async () => {
const matchMock = {
params: {
person: undefined,
},
}
render(
<MemoryRouter initialEntries={['/people']}>
<MockedProvider mocks={graphqlMocks} addTypename={false}>
<PeoplePage match={matchMock} />
</MockedProvider>
</MemoryRouter>
)
expect(screen.getByTestId('Layout')).toBeInTheDocument()
expect(screen.getByText('Recognize unlabeled faces')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('Person A')).toBeInTheDocument()
expect(screen.getByText('Person B')).toBeInTheDocument()
})
expect(
screen
.getAllByRole('link')
.some(x => x.getAttribute('href') == '/people/1')
).toBeTruthy()
expect(
screen
.getAllByRole('link')
.some(x => x.getAttribute('href') == '/people/3')
).toBeTruthy()
})
})
describe('FaceDetails component', () => {
const faceGroup: myFaces_myFaceGroups = {
id: '3',
label: null,
imageFaceCount: 2,
imageFaces: [
{
id: '3',
rectangle: {
minX: 0.2705079913139343,
maxX: 0.3408200144767761,
minY: 0.7691109776496887,
maxY: 0.881434977054596,
__typename: 'FaceRectangle',
},
media: {
id: '63',
thumbnail: {
url: 'http://localhost:4001/photo/thumbnail_image_jpg_p9x8dLWr.jpg',
width: 1024,
height: 641,
__typename: 'MediaURL',
},
__typename: 'Media',
},
__typename: 'ImageFace',
},
],
__typename: 'FaceGroup',
}
render(
<MemoryRouter initialEntries={['/people']}>
<MockedProvider mocks={graphqlMocks} addTypename={false}>
<PeoplePage match={matchMock} />
test('unlabeled, no images', () => {
const emptyFaceGroup: myFaces_myFaceGroups = {
...faceGroup,
imageFaces: [],
}
render(
<MockedProvider mocks={[]} addTypename={false}>
<FaceDetails group={emptyFaceGroup} />
</MockedProvider>
</MemoryRouter>
)
)
expect(screen.getByTestId('Layout')).toBeInTheDocument()
expect(screen.getByText('Recognize unlabeled faces')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('Person A')).toBeInTheDocument()
expect(screen.getByText('Person B')).toBeInTheDocument()
expect(screen.getByText('Unlabeled')).toBeInTheDocument()
})
expect(
screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/1')
).toBeTruthy()
test('labeled, with thumbnail', () => {
const labeledFaceGroup: myFaces_myFaceGroups = {
...faceGroup,
label: 'Some label',
}
expect(
screen.getAllByRole('link').some(x => x.getAttribute('href') == '/people/3')
).toBeTruthy()
render(
<MockedProvider mocks={[]} addTypename={false}>
<FaceDetails group={labeledFaceGroup} />
</MockedProvider>
)
expect(screen.getByText(labeledFaceGroup.label!)).toBeInTheDocument()
expect(screen.queryByText('Unlabeled')).not.toBeInTheDocument()
})
test('add label to face group', async () => {
const graphqlMocks = [
{
request: {
query: SET_GROUP_LABEL_MUTATION,
variables: {
groupID: '3',
label: 'John Doe',
},
},
newData: jest.fn(() => ({
data: {
setFaceGroupLabel: {
__typename: 'FaceGroup',
id: '3',
label: 'John Doe',
},
},
})),
},
]
render(
<MockedProvider mocks={graphqlMocks} addTypename={false}>
<FaceDetails group={faceGroup} />
</MockedProvider>
)
const btn = screen.getByRole('button')
expect(btn).toBeInTheDocument()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
fireEvent.click(btn)
const input = screen.getByRole('textbox')
expect(input).toBeInTheDocument()
expect(input).toHaveValue('')
fireEvent.change(input, { target: { value: 'John Doe' } })
fireEvent.keyUp(input, { key: 'Enter', code: 'Enter' })
await waitFor(() => {
expect(graphqlMocks[0].newData).toHaveBeenCalled()
})
})
})

View File

@@ -86,7 +86,7 @@ type FaceDetailsProps = {
group: myFaces_myFaceGroups
}
const FaceDetails = ({ group }: FaceDetailsProps) => {
export const FaceDetails = ({ group }: FaceDetailsProps) => {
const { t } = useTranslation()
const [editLabel, setEditLabel] = useState(false)
const [inputValue, setInputValue] = useState(group.label ?? '')

View File

@@ -0,0 +1,104 @@
import { gql, useQuery } from '@apollo/client'
import React, { useEffect, useReducer } from 'react'
import PaginateLoader from '../../../components/PaginateLoader'
import PhotoGallery from '../../../components/photoGallery/PhotoGallery'
import { photoGalleryReducer } from '../../../components/photoGallery/photoGalleryReducer'
import useScrollPagination from '../../../hooks/useScrollPagination'
import FaceGroupTitle from './FaceGroupTitle'
import {
singleFaceGroup,
singleFaceGroupVariables,
} from './__generated__/singleFaceGroup'
export const SINGLE_FACE_GROUP = gql`
query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) {
faceGroup(id: $id) {
id
label
imageFaces(paginate: { limit: $limit, offset: $offset }) {
id
rectangle {
minX
maxX
minY
maxY
}
media {
id
type
title
thumbnail {
url
width
height
}
highRes {
url
}
favorite
}
}
}
}
`
type SingleFaceGroupProps = {
faceGroupID: string
}
const SingleFaceGroup = ({ faceGroupID }: SingleFaceGroupProps) => {
const { data, error, loading, fetchMore } = useQuery<
singleFaceGroup,
singleFaceGroupVariables
>(SINGLE_FACE_GROUP, {
variables: {
limit: 200,
offset: 0,
id: faceGroupID,
},
})
const [mediaState, dispatchMedia] = useReducer(photoGalleryReducer, {
presenting: false,
activeIndex: -1,
media: [],
})
const { containerElem, finished: finishedLoadingMore } =
useScrollPagination<singleFaceGroup>({
loading,
fetchMore,
data,
getItems: data => data.faceGroup.imageFaces,
})
useEffect(() => {
const media = data?.faceGroup.imageFaces.map(x => x.media) || []
dispatchMedia({ type: 'replaceMedia', media })
}, [data])
const faceGroup = data?.faceGroup
if (error) {
return <div>{error.message}</div>
}
return (
<div ref={containerElem}>
<FaceGroupTitle faceGroup={faceGroup} />
<div>
<PhotoGallery
loading={loading}
dispatchMedia={dispatchMedia}
mediaState={mediaState}
/>
<PaginateLoader
active={!finishedLoadingMore && !loading}
text="Loading more photos"
/>
</div>
</div>
)
}
export default SingleFaceGroup