diff --git a/ui/src/Pages/PeoplePage/PeoplePage.test.tsx b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx new file mode 100644 index 00000000..0d9db98f --- /dev/null +++ b/ui/src/Pages/PeoplePage/PeoplePage.test.tsx @@ -0,0 +1,102 @@ +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 { MockedProvider } from '@apollo/client/testing' +import { MemoryRouter } from 'react-router' + +require('../../localization').setupLocalization() + +jest.mock('../../hooks/useScrollPagination', () => + jest.fn(() => ({ + finished: true, + containerElem: jest.fn(), + })) +) + +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, + }, + }, + }, + ], + }, + { + __typename: 'FaceGroup', + id: '1', + label: 'Person B', + imageFaceCount: 1, + imageFaces: [], + }, + ], + }, + }, + }, +] + +test('people page', async () => { + const matchMock = { + params: { + person: undefined, + }, + } + + render( + + + + + + ) + + 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() +}) diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.tsx similarity index 76% rename from ui/src/Pages/PeoplePage/PeoplePage.js rename to ui/src/Pages/PeoplePage/PeoplePage.tsx index 991447bd..2a625cd2 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.tsx @@ -1,5 +1,4 @@ import React, { createRef, useEffect, useState } from 'react' -import PropTypes from 'prop-types' import { gql, useMutation, useQuery } from '@apollo/client' import Layout from '../../Layout' import styled from 'styled-components' @@ -10,6 +9,16 @@ import FaceCircleImage from './FaceCircleImage' import useScrollPagination from '../../hooks/useScrollPagination' import PaginateLoader from '../../components/PaginateLoader' import { useTranslation } from 'react-i18next' +import { + setGroupLabel, + setGroupLabelVariables, +} from './__generated__/setGroupLabel' +import { + myFaces, + myFacesVariables, + myFaces_myFaceGroups, +} from './__generated__/myFaces' +import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces' export const MY_FACES_QUERY = gql` query myFaces($limit: Int, $offset: Int) { @@ -55,7 +64,7 @@ const RECOGNIZE_UNLABELED_FACES_MUTATION = gql` } ` -const FaceDetailsButton = styled.button` +const FaceDetailsButton = styled.button<{ labeled: boolean }>` color: ${({ labeled }) => (labeled ? 'black' : '#aaa')}; width: 150px; margin: 12px auto 24px; @@ -73,13 +82,20 @@ const FaceDetailsButton = styled.button` const FaceLabel = styled.span`` -const FaceDetails = ({ group }) => { +type FaceDetailsProps = { + group: myFaces_myFaceGroups +} + +const FaceDetails = ({ group }: FaceDetailsProps) => { const { t } = useTranslation() const [editLabel, setEditLabel] = useState(false) const [inputValue, setInputValue] = useState(group.label ?? '') - const inputRef = createRef() + const inputRef = createRef() - const [setGroupLabel, { loading }] = useMutation(SET_GROUP_LABEL_MUTATION, { + const [setGroupLabel, { loading }] = useMutation< + setGroupLabel, + setGroupLabelVariables + >(SET_GROUP_LABEL_MUTATION, { variables: { groupID: group.id, }, @@ -91,9 +107,7 @@ const FaceDetails = ({ group }) => { } useEffect(() => { - if (inputRef.current) { - inputRef.current.focus() - } + inputRef.current?.focus() }, [inputRef]) useEffect(() => { @@ -102,7 +116,7 @@ const FaceDetails = ({ group }) => { } }, [loading]) - const onKeyUp = e => { + const onKeyUp = (e: React.ChangeEvent & KeyboardEvent) => { if (e.key == 'Escape') { resetLabel() return @@ -111,6 +125,7 @@ const FaceDetails = ({ group }) => { if (e.key == 'Enter') { setGroupLabel({ variables: { + groupID: group.id, label: e.target.value == '' ? null : e.target.value, }, }) @@ -155,10 +170,6 @@ const FaceDetails = ({ group }) => { return label } -FaceDetails.propTypes = { - group: PropTypes.object.isRequired, -} - const FaceImagesCount = styled.span` background-color: #eee; color: #222; @@ -179,7 +190,11 @@ const EditIcon = styled(Icon)` } ` -const FaceGroup = ({ group }) => { +type FaceGroupProps = { + group: myFaces_myFaceGroups +} + +const FaceGroup = ({ group }: FaceGroupProps) => { const previewFace = group.imageFaces[0] return ( @@ -192,10 +207,6 @@ const FaceGroup = ({ group }) => { ) } -FaceGroup.propTypes = { - group: PropTypes.any, -} - const FaceGroupsWrapper = styled.div` display: flex; flex-wrap: wrap; @@ -204,27 +215,29 @@ const FaceGroupsWrapper = styled.div` const PeopleGallery = () => { const { t } = useTranslation() - const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, { + const { data, error, loading, fetchMore } = useQuery< + myFaces, + myFacesVariables + >(MY_FACES_QUERY, { variables: { limit: 50, offset: 0, }, }) - const [ - recognizeUnlabeled, - { loading: recognizeUnlabeledLoading }, - ] = useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION) + const [recognizeUnlabeled, { loading: recognizeUnlabeledLoading }] = + useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION) - const { containerElem, finished: finishedLoadingMore } = useScrollPagination({ - loading, - fetchMore, - data, - getItems: data => data.myFaceGroups, - }) + const { containerElem, finished: finishedLoadingMore } = + useScrollPagination({ + loading, + fetchMore, + data, + getItems: data => data.myFaceGroups, + }) if (error) { - return error.message + return
{error.message}
} let faces = null @@ -258,11 +271,20 @@ const PeopleGallery = () => { ) } -const PeoplePage = ({ match }) => { +type PeoplePageProps = { + match: { + params: { + person?: string + } + } +} + +const PeoplePage = ({ match }: PeoplePageProps) => { + const { t } = useTranslation() const faceGroup = match.params.person if (faceGroup) { return ( - + ) @@ -271,8 +293,4 @@ const PeoplePage = ({ match }) => { } } -PeoplePage.propTypes = { - match: PropTypes.object.isRequired, -} - export default PeoplePage diff --git a/ui/src/Pages/SharePage/SharePage.test.js b/ui/src/Pages/SharePage/SharePage.test.js index bd857581..1dbf8d9e 100644 --- a/ui/src/Pages/SharePage/SharePage.test.js +++ b/ui/src/Pages/SharePage/SharePage.test.js @@ -110,7 +110,7 @@ describe('load correct share page, based on graphql query', () => { expect(screen.getByText('Loading...')).toBeInTheDocument() - await waitForElementToBeRemoved(() => screen.getByText('Loading...')) + await waitForElementToBeRemoved(() => screen.queryByText('Loading...')) expect(screen.getByTestId('Layout')).toBeInTheDocument() expect(screen.getByTestId('MediaSharePage')).toBeInTheDocument()