Skip to content

Commit

Permalink
feat: add testimonials to project pages
Browse files Browse the repository at this point in the history
  • Loading branch information
dmijatovic committed Sep 5, 2024
1 parent f8cc6e6 commit 56efe2b
Show file tree
Hide file tree
Showing 21 changed files with 1,165 additions and 229 deletions.
21 changes: 21 additions & 0 deletions data-generation/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,26 @@ function generateTestimonials(ids) {
software: id,
message: faker.hacker.phrase(),
source: faker.person.fullName(),
position: index + 1,
});
}
}

return result;
}

function generateProjectTestimonials(ids) {
const result = [];

for (const id of ids) {
// each project will get 0, 1 or 2 testimonials
const numberOfTestimonials = faker.number.int({max: 3, min: 0});
for (let index = 0; index < numberOfTestimonials; index++) {
result.push({
project: id,
message: faker.hacker.phrase(),
source: faker.person.fullName(),
position: index + 1,
});
}
}
Expand Down Expand Up @@ -1019,6 +1039,7 @@ const projectPromise = postToBackend('/project', generateProjects())
idsProjects = pjArray.map(sw => sw['id']);
postToBackend('/team_member', await generateTeamMembers(idsProjects, peopleWithOrcid));
postToBackend('/url_for_project', generateUrlsForProjects(idsProjects));
postToBackend('/testimonial_for_project', generateProjectTestimonials(idsProjects));
postToBackend('/keyword_for_project', generateKeywordsForEntity(idsProjects, idsKeywords, 'project'));
postToBackend('/output_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project'));
postToBackend('/impact_for_project', generateMentionsForEntity(idsProjects, idsMentions, 'project'));
Expand Down
31 changes: 31 additions & 0 deletions database/007-create-relations-for-projects.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
-- SPDX-FileCopyrightText: 2022 - 2024 Netherlands eScience Center
-- SPDX-FileCopyrightText: 2022 Dusan Mijatovic (dv4all)
-- SPDX-FileCopyrightText: 2022 dv4all
-- SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
--
-- SPDX-License-Identifier: Apache-2.0

Expand Down Expand Up @@ -45,3 +46,33 @@ END
$$;

CREATE TRIGGER sanitise_update_team_member BEFORE UPDATE ON team_member FOR EACH ROW EXECUTE PROCEDURE sanitise_update_team_member();


CREATE TABLE testimonial_for_project (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
project UUID REFERENCES project (id) NOT NULL,
message VARCHAR(500) NOT NULL,
source VARCHAR(200) NOT NULL,
position INTEGER
);

CREATE FUNCTION sanitise_insert_testimonial_for_project() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
NEW.id = gen_random_uuid();
return NEW;
END
$$;

CREATE TRIGGER sanitise_insert_testimonial_for_project BEFORE INSERT ON testimonial_for_project FOR EACH ROW EXECUTE PROCEDURE sanitise_insert_testimonial_for_project();


CREATE FUNCTION sanitise_update_testimonial_for_project() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
NEW.id = OLD.id;
return NEW;
END
$$;

CREATE TRIGGER sanitise_update_testimonial_for_project BEFORE UPDATE ON testimonial_for_project FOR EACH ROW EXECUTE PROCEDURE sanitise_update_testimonial_for_project();
12 changes: 12 additions & 0 deletions frontend/components/projects/edit/editProjectPages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import TerminalIcon from '@mui/icons-material/Terminal'
import ContentLoader from '~/components/layout/ContentLoader'
import JoinInnerIcon from '@mui/icons-material/JoinInner'
import AddCommentIcon from '@mui/icons-material/AddComment'
import ThreePIcon from '@mui/icons-material/ThreeP'

// use dynamic imports
const ProjectInformation = dynamic(() => import('./information'),{
Expand All @@ -36,6 +37,10 @@ const RelatedProjects = dynamic(() => import('./related-projects'),{
const RelatedSoftware = dynamic(() => import('./related-software'),{
loading: ()=><ContentLoader />
})
const ProjectTestimonials = dynamic(() => import('./testimonials'),{
loading: ()=><ContentLoader />
})

const ProjectMaintainers = dynamic(() => import('./maintainers'),{
loading: ()=><ContentLoader />
})
Expand Down Expand Up @@ -77,6 +82,13 @@ export const editProjectPage: EditProjectPageProps[] = [
render: () => <ProjectMentions />,
status: ''
},
{
id: 'testimonials',
label: 'Testimonials',
icon: <ThreePIcon />,
render: () => <ProjectTestimonials />,
status: ''
},
{
id: 'related-projects',
label: 'Related projects',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all)
// SPDX-FileCopyrightText: 2023 Dusan Mijatovic (dv4all) (dv4all)
// SPDX-FileCopyrightText: 2023 dv4all
// SPDX-FileCopyrightText: 2024 Dusan Mijatovic (Netherlands eScience Center)
// SPDX-FileCopyrightText: 2024 Netherlands eScience Center
//
// SPDX-License-Identifier: Apache-2.0

import {fireEvent, render, screen, waitFor, waitForElementToBeRemoved, within} from '@testing-library/react'

import {WithAppContext, mockSession} from '~/utils/jest/WithAppContext'
import {WithProjectContext} from '~/utils/jest/WithProjectContext'
import {initialState as projectState} from '~/components/projects/edit/editProjectContext'

import SoftwareTestimonials from './index'
import config from './config'

// MOCKS
import mockTestimonials from './__mocks__/project_testimonials.json'

// Mock editTestimonial api calls
const mockGetTestimonialsForProject = jest.fn(props => Promise.resolve(mockTestimonials))
const mockPostTestimonial = jest.fn(({testimonial}) => {
return Promise.resolve({
status: 201,
message: testimonial
})
})
const mockDeleteProjectTestimonial = jest.fn(props => Promise.resolve([] as any))
const mockPatchTestimonialPositions = jest.fn(props => Promise.resolve([] as any))
jest.mock('./apiProjectTestimonial', () => ({
getTestimonialsForProject: jest.fn(props => mockGetTestimonialsForProject(props)),
addProjectTestimonial: jest.fn(props => mockPostTestimonial(props)),
deleteProjectTestimonial: jest.fn(props => mockDeleteProjectTestimonial(props)),
patchTestimonialPositions: jest.fn(props => mockPatchTestimonialPositions(props)),
}))

describe('frontend/components/projects/edit/testimonials/index.tsx', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('renders no testimonials message', async() => {
// required prop
projectState.project.id = 'test-project-id'
// return no items
mockGetTestimonialsForProject.mockResolvedValueOnce([])

render(
<WithAppContext options={{session:mockSession}}>
<WithProjectContext state={projectState}>
<SoftwareTestimonials />
</WithProjectContext>
</WithAppContext>
)

// wait for loader to
await waitForElementToBeRemoved(screen.getByRole('progressbar'))
// validate no items message
const noItemsMsg = screen.getByText('No testimonials')
})

it('renders mocked testimonials', async() => {
// required prop
projectState.project.id = 'test-project-id'
// return no items
mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials)

render(
<WithAppContext options={{session:mockSession}}>
<WithProjectContext state={projectState}>
<SoftwareTestimonials />
</WithProjectContext>
</WithAppContext>
)

// wait for loader to
await waitForElementToBeRemoved(screen.getByRole('progressbar'))

// validate number of items
const testimonials = screen.getAllByTestId('testimonial-list-item')
expect(testimonials.length).toEqual(mockTestimonials.length)
// validate first item message
expect(testimonials[0]).toHaveTextContent(mockTestimonials[0].message)
})

it('can add testimonial', async() => {
// required prop
projectState.project.id = 'test-project-id'
// return no items
mockGetTestimonialsForProject.mockResolvedValueOnce([])

const newItem = {
message: 'This is test message',
source: 'This is test source'
}

render(
<WithAppContext options={{session:mockSession}}>
<WithProjectContext state={projectState}>
<SoftwareTestimonials />
</WithProjectContext>
</WithAppContext>
)

// wait for loader to
await waitForElementToBeRemoved(screen.getByRole('progressbar'))

// click add button
const addBtn = screen.getByRole('button', {
name: 'Add'
})
fireEvent.click(addBtn)

// get modal
const modal = screen.getByRole('dialog')

// write message
const message = screen.getByRole('textbox', {
name: config.message.label,
})
fireEvent.change(message, {target: {value: newItem.message}})

// write source
const source = screen.getByRole('textbox', {
name: config.source.label,
})
fireEvent.change(source, {target: {value: newItem.source}})

// click on save
const saveBtn = screen.getByRole('button', {
name: 'Save'
})
await waitFor(() => {
expect(saveBtn).toBeEnabled()
fireEvent.click(saveBtn)
})

// validate api call
await waitFor(() => {
expect(mockPostTestimonial).toBeCalledTimes(1)
expect(mockPostTestimonial).toBeCalledWith({
'testimonial': {
'id': null,
'message': newItem.message,
'position': 1,
'project': projectState.project.id,
'source': newItem.source,
},
'token': 'TEST_TOKEN'
})
})
})

it('can edit testimonial', async() => {
// required prop
projectState.project.id = 'test-project-id'
// return no items
mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials)

render(
<WithAppContext options={{session:mockSession}}>
<WithProjectContext state={projectState}>
<SoftwareTestimonials />
</WithProjectContext>
</WithAppContext>
)

// wait for loader to
await waitForElementToBeRemoved(screen.getByRole('progressbar'))

// get items
const testimonials = screen.getAllByTestId('testimonial-list-item')
// edit btn
const editBtn = within(testimonials[0]).getByRole('button', {
name: 'edit'
})
fireEvent.click(editBtn)

// confirm modal has values
const modal = screen.getByRole('dialog')

// validate message value
const message = screen.getByRole('textbox', {
name: config.message.label,
})
expect(message).toHaveValue(mockTestimonials[0].message)

// validate source value
const source = screen.getByRole('textbox', {
name: config.source.label,
})
expect(source).toHaveValue(mockTestimonials[0].source)

// Cancel update action
const cancelBtn = screen.getByRole('button', {
name: 'Cancel'
})
fireEvent.click(cancelBtn)
// validate modal is closed
expect(modal).not.toBeVisible()
})

it('can delete testimonial', async () => {
// required prop
projectState.project.id = 'test-project-id'
// return no items
mockGetTestimonialsForProject.mockResolvedValueOnce(mockTestimonials)
// mock delete response
mockDeleteProjectTestimonial.mockResolvedValueOnce({
status: 200,
message: 'OK'
})
// mock patch response
mockPatchTestimonialPositions.mockResolvedValueOnce({
status: 200,
message: 'OK'
})

render(
<WithAppContext options={{session: mockSession}}>
<WithProjectContext state={projectState}>
<SoftwareTestimonials />
</WithProjectContext>
</WithAppContext>
)

// wait for loader to
await waitForElementToBeRemoved(screen.getByRole('progressbar'))

// get items
const testimonials = screen.getAllByTestId('testimonial-list-item')
// edit btn
const deleteBtn = within(testimonials[0]).getByRole('button', {
name: 'delete'
})
fireEvent.click(deleteBtn)

// confirm modal
const confirmModal = screen.getByRole('dialog', {
name: 'Remove testimonial'
})
// click on Remove button
const removeBtn = within(confirmModal).getByRole('button', {
name: 'Remove'
})
fireEvent.click(removeBtn)

// validate api calls
await waitFor(() => {
// validate delete testimonial api
expect(mockDeleteProjectTestimonial).toBeCalledTimes(1)
expect(mockDeleteProjectTestimonial).toBeCalledWith({
'id': mockTestimonials[0].id,
'token': mockSession.token,
})
// validate patch testimonial positions called
expect(mockPatchTestimonialPositions).toBeCalledTimes(1)
})

await waitFor(async() => {
// validate item removed from list
const remained = await screen.findAllByTestId('testimonial-list-item')
expect(remained.length).toEqual(testimonials.length - 1)
})
})
})

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"id": "eec59b21-1fb6-4e63-830b-fcf837c812c1",
"project": "2930a586-6781-4091-af8e-7411f9be76d4",
"message": "This is my testimonal for msedge 1",
"source": "Me, Here, Today",
"position": 1
},
{
"id": "e53e1572-0aad-4b5d-817b-219b20138c57",
"project": "2930a586-6781-4091-af8e-7411f9be76d4",
"message": "This is my testimonal for msedge 2",
"source": "You, There, Yesterday",
"position": 2
}
]
Loading

0 comments on commit 56efe2b

Please sign in to comment.