Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
transition: transform 0.3s;
overflow: scroll;
max-height: 90vh;


}
&.modal-lg .modal {
width: 800px;
}

.modal-close-x {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface RadioGroupProps {
name: string
selected: string
setSelected: (value) => void
additionalText?: string
}

const get = (obj, property) => (typeof obj === "object" ? obj[property] : obj)
Expand All @@ -27,6 +28,7 @@ export const RadioGroup = ({
name,
selected,
setSelected,
additionalText,
}: RadioGroupProps) => {
return (
<div className={"on-radio-wrapper" + " " + layout}>
Expand All @@ -40,6 +42,8 @@ export const RadioGroup = ({
onChange={(e) => setSelected(e.target.value)}
/>
))}
{additionalText && <div className="additional-text">{additionalText}
</div>}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@
}
}
}
.additional-text{
margin: -11px 5px 30px;
font-size: 14px;
padding: 0;
width: 100%;
}
}

.on-radio-wrapper.chiclet {
Expand Down
5 changes: 5 additions & 0 deletions packages/openneuro-app/src/scripts/queries/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,32 @@ export const GET_USER = gql`
blocked
githubSynced
github
orcidConsent
}
}
`

// GraphQL mutation to update user data
export const UPDATE_USER = gql`
mutation updateUser(
$id: ID!
$location: String
$links: [String]
$institution: String
$orcidConsent: Boolean
) {
updateUser(
id: $id
location: $location
links: $links
institution: $institution
orcidConsent: $orcidConsent
) {
id
location
links
institution
orcidConsent
}
}
`
Expand Down
1 change: 1 addition & 0 deletions packages/openneuro-app/src/scripts/types/user-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface User {
provider?: string
modified?: string
githubSynced?: Date
orcidConsent?: boolean | null
}

export interface UserRoutesProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
import { vi } from "vitest"
import React from "react"
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { MockedProvider } from "@apollo/client/testing"
import type { MockedResponse } from "@apollo/client/testing"
import {
OrcidConsentForm,
type OrcidConsentFormProps,
} from "../components/orcid-consent-form"
import { GET_USER, UPDATE_USER } from "../../queries/user"

// Mock Button component
vi.mock("../../components/button/Button", () => ({
Button: vi.fn(({ label, onClick, disabled, primary, size }) => (
<button
data-testid="mock-save-button"
onClick={onClick}
disabled={disabled}
className={`${primary ? "primary" : ""} ${size}`}
>
{label}
</button>
)),
}))

// Mock RadioGroup component
vi.mock("../../components/radio/RadioGroup", () => ({
RadioGroup: vi.fn((
{ name, radioArr, selected, setSelected, additionalText },
) => (
<div data-testid={`mock-radio-group-${name}`}>
{radioArr.map((item) => (
<label key={item.value}>
<input
type="radio"
name={name}
value={item.value}
checked={selected === item.value}
onChange={(e) => setSelected(e.target.value)}
data-testid={`radio-${item.value}`}
/>
{item.label}
</label>
))}
{additionalText && (
<p data-testid="radio-additional-text">{additionalText}</p>
)}
</div>
)),
}))

describe("OrcidConsentForm", () => {
const mockUserId = "test-user-id-123"
const mockOnConsentUpdated = vi.fn()
const mockConsoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {})

// Mock Apollo Client's useMutation for UPDATE_USER
const mockUpdateUserMutation = {
request: {
query: UPDATE_USER,
variables: { id: mockUserId, orcidConsent: true },
},
result: {
data: {
updateUser: {
id: mockUserId,
location: null,
links: [],
institution: null,
orcidConsent: true,
__typename: "User",
},
},
},
}

const mockUpdateUserMutationFalse = {
request: {
query: UPDATE_USER,
variables: { id: mockUserId, orcidConsent: false },
},
result: {
data: {
updateUser: {
id: mockUserId,
location: null,
links: [],
institution: null,
orcidConsent: false,
__typename: "User",
},
},
},
}

// Mock GET_USER for refetchQueries
const mockGetUserQueryTrue = {
request: {
query: GET_USER,
variables: { userId: mockUserId },
},
result: {
data: {
user: {
id: mockUserId,
name: "Test User",
email: "[email protected]",
orcidConsent: true,
__typename: "User",
},
},
},
}
const mockGetUserQueryFalse = {
request: {
query: GET_USER,
variables: { userId: mockUserId },
},
result: {
data: {
user: {
id: mockUserId,
name: "Test User",
email: "[email protected]",
orcidConsent: false,
__typename: "User",
},
},
},
}
const mockGetUserQueryNull = {
request: {
query: GET_USER,
variables: { userId: mockUserId },
},
result: {
data: {
user: {
id: mockUserId,
name: "Test User",
email: "[email protected]",
orcidConsent: null,
__typename: "User",
},
},
},
}

beforeEach(() => {
vi.clearAllMocks()
mockConsoleWarn.mockClear()
})

afterAll(() => {
mockConsoleWarn.mockRestore()
})

const renderComponent = (
props: Partial<OrcidConsentFormProps> = {},
mocks: MockedResponse[] = [],
) => {
const defaultProps: OrcidConsentFormProps = {
userId: mockUserId,
initialOrcidConsent: null,
onConsentUpdated: mockOnConsentUpdated,
}
// Combine component-specific mocks with any test-specific mocks
const combinedMocks = [
mockUpdateUserMutation,
mockUpdateUserMutationFalse,
mockGetUserQueryTrue,
mockGetUserQueryFalse,
mockGetUserQueryNull,
...mocks,
]

return render(
<MockedProvider mocks={combinedMocks} addTypename={false}>
<OrcidConsentForm {...defaultProps} {...props} />
</MockedProvider>,
)
}

it("renders correctly with initial null consent and hides save button", () => {
renderComponent({ initialOrcidConsent: null })
expect(screen.getByTestId("mock-radio-group-orcidConsent-modal"))
.toBeInTheDocument()
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument()
expect(screen.getByTestId("radio-true")).not.toBeChecked()
expect(screen.getByTestId("radio-false")).not.toBeChecked()
})

it("shows the save button when a radio option is selected (from initial null)", async () => {
renderComponent({ initialOrcidConsent: null })
const radioConsent = screen.getByTestId("radio-true")
fireEvent.click(radioConsent)
await waitFor(() => {
const saveButton = screen.getByTestId("mock-save-button")
expect(saveButton).toBeInTheDocument()
expect(saveButton).not.toBeDisabled()
expect(saveButton).toHaveTextContent("Save Consent")
expect(screen.getByTestId("radio-true")).toBeChecked()
})
})

it("after save button clicked, the new query (simulated prop update) reflects the selected consent", async () => {
// Scenario 1: Change from initial `false` to `true`
// Start with initialOrcidConsent: false
const { rerender } = renderComponent({ initialOrcidConsent: false })
// Initial state check
expect(screen.getByTestId("radio-false")).toBeChecked()
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument()
// User selects "I consent" (true)
fireEvent.click(screen.getByTestId("radio-true"))
// Save button should appear now
const saveButton = await screen.findByTestId("mock-save-button")
expect(saveButton).toBeInTheDocument()
// Click save
fireEvent.click(saveButton)
await waitFor(() => {
expect(mockOnConsentUpdated).toHaveBeenCalledWith(true)
})

rerender(
<MockedProvider mocks={[mockGetUserQueryTrue]} addTypename={false}>
<OrcidConsentForm
userId={mockUserId}
initialOrcidConsent={true}
onConsentUpdated={mockOnConsentUpdated}
/>
</MockedProvider>,
)
// Assert the UI reflects the new `true` state from the prop
await waitFor(() => {
expect(screen.getByTestId("radio-true")).toBeChecked()
expect(screen.getByTestId("radio-false")).not.toBeChecked()
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument()
})

// Scenario 2: Change from initial `true` to `false`
mockOnConsentUpdated.mockClear()
// Reset component to initial `true`
rerender(
<MockedProvider mocks={[mockGetUserQueryFalse]} addTypename={false}>
<OrcidConsentForm
userId={mockUserId}
initialOrcidConsent={true}
onConsentUpdated={mockOnConsentUpdated}
/>
</MockedProvider>,
)
// Initial state check for second scenario
await waitFor(() => {
expect(screen.getByTestId("radio-true")).toBeChecked()
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument()
})

// User selects "I DO NOT consent" (false)
fireEvent.click(screen.getByTestId("radio-false"))
// Save button should appear
const saveButtonFalse = await screen.findByTestId("mock-save-button")
expect(saveButtonFalse).toBeInTheDocument()
// Click save
fireEvent.click(saveButtonFalse)
await waitFor(() => {
expect(mockOnConsentUpdated).toHaveBeenCalledWith(false)
})
rerender(
<MockedProvider mocks={[mockGetUserQueryFalse]} addTypename={false}>
<OrcidConsentForm
userId={mockUserId}
initialOrcidConsent={false}
onConsentUpdated={mockOnConsentUpdated}
/>
</MockedProvider>,
)
// Assert the UI reflects the new `false` state from the prop
await waitFor(() => {
expect(screen.getByTestId("radio-false")).toBeChecked()
expect(screen.getByTestId("radio-true")).not.toBeChecked()
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument() // Button hidden
})
})

it("does not show the save button if no consent option is selected initially", () => {
renderComponent({ initialOrcidConsent: null })
expect(screen.queryByTestId("mock-save-button")).not.toBeInTheDocument()
expect(screen.getByTestId("radio-true")).not.toBeChecked()
expect(screen.getByTestId("radio-false")).not.toBeChecked()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "../scss/editable-content.scss"
interface EditableContentProps {
editableContent: string[] | string
setRows: React.Dispatch<React.SetStateAction<string[] | string>>
className: string
className?: string
heading: string
validation?: RegExp | ((value: string) => boolean)
validationMessage?: string
Expand Down
Loading