diff --git a/__mock__/@zooniverse/panoptes-js.js b/__mock__/@zooniverse/panoptes-js.js new file mode 100644 index 00000000..699a15b4 --- /dev/null +++ b/__mock__/@zooniverse/panoptes-js.js @@ -0,0 +1,9 @@ +const subjects = { + get: (subjectId) => { + return Promise.resolve(null) + } +} + +module.exports = { + subjects +} \ No newline at end of file diff --git a/__mock__/README.md b/__mock__/README.md new file mode 100644 index 00000000..3f95f158 --- /dev/null +++ b/__mock__/README.md @@ -0,0 +1,11 @@ +# Jest Mocks + +This folder contains modules mocked for Jest tests. + +If one of our components has `import { banana } from 'fruitlist'`, and that fruitlist module is complex, server-reliant, or otherwise borking our tests, then expect to find a `fruitlist.js` file in here. + +Additional notes (accurate as of Jest 29.6.4): + +- The `__mock__` folder is _implicit_ to Jest: https://jestjs.io/docs/manual-mocks +- The `__mock__` folder can handle scoped modules, e.g. `@zooniverse/panoptes-js`, by using subfolders. +- However, if the `import` command is pulling from _subfolders,_ then the `__mock__` folder doesn't seem to function as well. See `App.spec.js` for how it handles `import auth from 'panoptes-client/lib/auth'` using `jest.mock('panoptes-client/lib/auth', ...)` \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..bfe9bc8c --- /dev/null +++ b/jest.config.js @@ -0,0 +1,26 @@ +const config = { + 'moduleNameMapper': { + // Maps all media files to a mock file. + // Without this, test will crash with a "Jest encountered an unexpected token" due to Jest having no idea what to do with binary files. + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '/jest/mockMediaFile.js', + + // Maps the @src alias added in webpack. + '^@src/(.*)$': '/src/$1' + }, + + // Tells Jest to look for unit test files in the /src directory. + 'roots': [ + '/src' + ], + + 'setupFilesAfterEnv': [ + '/jest/jest-setup.js' + ], + + // Without this, render() will result in "document is not defined" errors. + 'testEnvironment': 'jsdom', +} + +// Jest doesn't like 'export' (i.e. ES6 modules), so we use module.exports (CommonJS modules). +// Probably because it's running in Node.js. +module.exports = config \ No newline at end of file diff --git a/jest.config.json b/jest.config.json deleted file mode 100644 index 619fccf5..00000000 --- a/jest.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "moduleNameMapper": { - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/jest/mockFile.js", - "^@src/(.*)$": "/src/$1" - }, - "roots": [ - "/src" - ], - "setupFilesAfterEnv": ["/jest/jest-setup.js"], - "testEnvironment": "jsdom" -} diff --git a/jest/mockFile.js b/jest/mockMediaFile.js similarity index 100% rename from jest/mockFile.js rename to jest/mockMediaFile.js diff --git a/package.json b/package.json index 97d5df23..58a823c2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "export HEAD_COMMIT=${HEAD_COMMIT:-$(git rev-parse --short HEAD)} ; webpack --config webpack.prod.js", "start": "webpack serve --config webpack.dev.js", - "test": "jest" + "test": "jest --config jest.config.js" }, "repository": { "type": "git", diff --git a/src/App/App.spec.js b/src/App/App.spec.js index 2c91b26d..5a728fbb 100644 --- a/src/App/App.spec.js +++ b/src/App/App.spec.js @@ -3,9 +3,13 @@ import { MemoryRouter } from 'react-router-dom' import App from './App.js' // mock Panoptes auth calls -import auth from 'panoptes-client/lib/auth' -jest.mock('panoptes-client/lib/auth') -auth.checkCurrent.mockResolvedValue(null) +jest.mock('panoptes-client/lib/auth', () => { + return { + checkCurrent: () => { + return Promise.resolve(null) + } + } +}) describe('App', function () { test('should render logo', async function () { @@ -17,27 +21,4 @@ describe('App', function () { const logo = await screen.findByText(/Zooniverse Logo/i) expect(logo).toBeInTheDocument() }) - - /* - describe('when user isn\'t logged in', function () { - test('should display "isn\'t logged in" message', async function () { - auth.checkCurrent.mockResolvedValue(null) - render() - const userText = await screen.findByText(/User isn't logged in/i) - expect(userText).toBeInTheDocument() - }) - }) - - describe('when user is logged in', function () { - test('should display user details', async function () { - auth.checkCurrent.mockResolvedValue({ - display_name: 'Test User', - login: 'testuser', - }) - render() - const userText = await screen.findByText(/Logged in as Test User/i) - expect(userText).toBeInTheDocument() - }) - }) - */ }) diff --git a/src/components/ProjectContainer/ProjectContainer.js b/src/components/ProjectContainer/ProjectContainer.js index f7e5db2d..ab8d0805 100644 --- a/src/components/ProjectContainer/ProjectContainer.js +++ b/src/components/ProjectContainer/ProjectContainer.js @@ -7,7 +7,6 @@ import projectsJson from '@src/projects.json' export default function ProjectContainer ({}) { const store = useStores() - const params = useParams() const projectOwner = params.projectOwner || '' const projectName = params.projectName || '' diff --git a/src/components/ProjectContainer/ProjectContainer.spec.js b/src/components/ProjectContainer/ProjectContainer.spec.js new file mode 100644 index 00000000..dcc65c3b --- /dev/null +++ b/src/components/ProjectContainer/ProjectContainer.spec.js @@ -0,0 +1,42 @@ +import { render, screen } from '@testing-library/react' +import { MemoryRouter, Route, Routes } from 'react-router-dom' +import ProjectContainer from './ProjectContainer.js' + +import { AppContext } from '@src/store' + +describe('ProjectContainers', function () { + let selectedProject + const store = { + project: { + name: 'Placeholder Project', + slug: 'foo/bar', + id: '1234' + }, + user: null, + showingSensitiveContent: false, + setProject: (p) => { selectedProject = p }, + setUser: () => {}, + setShowingSensitiveContent: () => {}, + } + + test('should find (and set) a project based on the route/path', async function () { + selectedProject = null + + render( + + + + } /* Output: we expect ProjectContainer to call setProject() with the intended project */ + /> + + + + ) + + expect(selectedProject?.name).toEqual('Community Catalog (Stable Test Project)') + }) +}) diff --git a/src/components/SubjectImage/SubjectImage.spec.js b/src/components/SubjectImage/SubjectImage.spec.js new file mode 100644 index 00000000..1ce45936 --- /dev/null +++ b/src/components/SubjectImage/SubjectImage.spec.js @@ -0,0 +1,83 @@ +import { act, render, screen } from '@testing-library/react' +import SubjectImage from './SubjectImage.js' + +// Example data +const exampleSubject_87892456 = { + id: '87892456', + locations: [ + { 'image/jpeg': 'https://panoptes-uploads.zooniverse.org/subject_location/66fd834d-b13a-40f0-b97d-6d364841d56c.jpeg' }, + { 'image/jpeg': 'https://panoptes-uploads.zooniverse.org/subject_location/39c77068-22eb-472d-b280-1af9e2f33437.jpeg' } + ] +} + +// mock Panoptes module +jest.mock('@zooniverse/panoptes-js', () => { + return { + subjects: { + get: ({ id }) => { + if (id === '87892456') { + const mockResponse = { + body: { + subjects: [ exampleSubject_87892456 ] + }, + ok: true, + status: 200, + statusCode: 200, + } + + return Promise.resolve(mockResponse) + } + return Promise.resolve(null) + } + } + } +}) + +describe.only('SubjectImage', function () { + test('should render an image, when given a subject', function () { + render( + + ) + + // For multi-image subjects, SubjectImage will render the FIRST image. + expect(screen.getByRole('img')).toHaveProperty('alt', 'Preview image for Subject 87892456') + expect(screen.getByRole('img')).toHaveProperty('src', 'https://panoptes-uploads.zooniverse.org/subject_location/66fd834d-b13a-40f0-b97d-6d364841d56c.jpeg') + }) + + /* + TODO: this isn't working, as the SubjectImage seems to continue updating outside the act() + Perhaps we need to implement useSWR (https://github.com/zooniverse/community-catalog/pull/131) (to avoid using setState in useEffect), or look into other patterns that handle async calls. + (@shaunanoordin 20230826) + + test('should render an image, when given a subject ID', function () { + act(() => { + render( + + ) + }) + + // For multi-image subjects, SubjectImage will render the FIRST image. + expect(screen.getByRole('img')).toHaveProperty('alt', 'Preview image for Subject 87892456') + expect(screen.getByRole('img')).toHaveProperty('src', 'https://panoptes-uploads.zooniverse.org/subject_location/66fd834d-b13a-40f0-b97d-6d364841d56c.jpeg') + }) + /**/ + + test('should render a placeholder image, when given no subject', function () { + render( + + ) + + const svg = screen.getByLabelText('Placeholder for Subject image') + expect(svg).toBeDefined() + + const path = svg.querySelector('path') + expect(path).toHaveAttribute('d', 'M1 3h22v18H1V3zm5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm17 6-5-6-6 7-3-3-8 8') + expect(path).toHaveAttribute('fill', 'none') + expect(path).toHaveAttribute('stroke', '#000') + expect(path).toHaveAttribute('stroke-width', '2') + }) +}) diff --git a/src/router.js b/src/router.js index f96c9472..b5dc20f7 100644 --- a/src/router.js +++ b/src/router.js @@ -11,7 +11,7 @@ import SubjectPage from '@src/pages/SubjectPage' import getEnv from '@src/helpers/getEnv.js' import getQuery from '@src/helpers/getQuery.js' -export const router = createBrowserRouter([ +export const routes = [ { path: '/', element: , @@ -77,4 +77,6 @@ export const router = createBrowserRouter([ } ] }, -]) \ No newline at end of file +] + +export const router = createBrowserRouter(routes) \ No newline at end of file