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
3 changes: 2 additions & 1 deletion frontends/api/src/test-utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ const userSubscription = {
`${API_BASE_URL}/api/v1/learning_resources_user_subscription/check/${query(params)}`,
delete: (id: number) =>
`${API_BASE_URL}/api/v1/learning_resources_user_subscription/${id}/unsubscribe/`,
post: () => "/api/v1/learning_resources_user_subscription/subscribe/",
post: () =>
`${API_BASE_URL}/api/v1/learning_resources_user_subscription/subscribe/`,
}

const fields = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect } from "react"
import React, { useEffect, useCallback } from "react"
import {
RoutedDrawer,
LearningResourceExpanded,
Expand Down Expand Up @@ -96,10 +96,13 @@ const useOpenLearningResourceDrawer = () => {
const useResourceDrawerHref = () => {
const [search] = useSearchParams()

return (id: number) => {
search.set(RESOURCE_DRAWER_QUERY_PARAM, id.toString())
return `?${search.toString()}`
}
return useCallback(
(id: number) => {
search.set(RESOURCE_DRAWER_QUERY_PARAM, id.toString())
return `?${search.toString()}`
},
[search],
)
}

export default LearningResourceDrawer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from "react"
import * as NiceModal from "@ebay/nice-modal-react"
import { renderWithProviders, user, screen } from "../../test-utils"
import type { User } from "../../test-utils"
import { ResourceCard, ResourceListCard } from "./ResourceCard"
import {
AddToLearningPathDialog,
AddToUserListDialog,
} from "../Dialogs/AddToListDialog"
import type { ResourceCardProps } from "./ResourceCard"
import { urls, factories, setMockResponse } from "api/test-utils"
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
import invariant from "tiny-invariant"

jest.mock("@ebay/nice-modal-react", () => {
const actual = jest.requireActual("@ebay/nice-modal-react")
return {
__esModule: true,
...actual,
show: jest.fn(),
}
})

describe.each([
{
CardComponent: ResourceCard,
},
{
CardComponent: ResourceListCard,
},
])("$CardComponent", ({ CardComponent }) => {
const makeResource = factories.learningResources.resource
type SetupOptions = {
user?: Partial<User>
props?: Partial<ResourceCardProps>
}
const setup = ({ user, props = {} }: SetupOptions = {}) => {
const { resource = makeResource() } = props
if (user?.is_authenticated) {
setMockResponse.get(urls.userMe.get(), user)
} else {
setMockResponse.get(urls.userMe.get(), {}, { code: 403 })
}
const { view, location } = renderWithProviders(
<CardComponent {...props} resource={resource} />,
)
return { resource, view, location }
}

const labels = {
addToLearningPaths: "Add to Learning Path",
addToUserList: "Add to User List",
}

test("Applies className to the resource card", () => {
const { view } = setup({ user: {}, props: { className: "test-class" } })
expect(view.container.firstChild).toHaveClass("test-class")
})

test.each([
{
user: { is_authenticated: true, is_learning_path_editor: false },
expectAddToLearningPathButton: false,
},
{
user: { is_authenticated: true, is_learning_path_editor: true },
expectAddToLearningPathButton: true,
},
{
user: { is_authenticated: false },
expectAddToLearningPathButton: false,
},
])(
"Always shows 'Add to User List' button, but only shows 'Add to Learning Path' button if user is a learning path editor",
async ({ user, expectAddToLearningPathButton }) => {
setup({ user })
await screen.findByRole("button", {
name: labels.addToUserList,
})
const addToLearningPathButton = screen.queryByRole("button", {
name: labels.addToLearningPaths,
})
expect(!!addToLearningPathButton).toBe(expectAddToLearningPathButton)
},
)

test("Clicking add to list button opens AddToListDialog when authenticated", async () => {
const showModal = jest.mocked(NiceModal.show)

const { resource } = setup({
user: { is_learning_path_editor: true, is_authenticated: true },
})
const addToUserListButton = await screen.findByRole("button", {
name: labels.addToUserList,
})
const addToLearningPathButton = await screen.findByRole("button", {
name: labels.addToLearningPaths,
})

expect(showModal).not.toHaveBeenCalled()
await user.click(addToLearningPathButton)
invariant(resource)
expect(showModal).toHaveBeenLastCalledWith(AddToLearningPathDialog, {
resourceId: resource.id,
})
await user.click(addToUserListButton)
expect(showModal).toHaveBeenLastCalledWith(AddToUserListDialog, {
resourceId: resource.id,
})
})

test("Clicking 'Add to User List' opens signup popover if not authenticated", async () => {
setup({
user: { is_authenticated: false },
})
const addToUserListButton = await screen.findByRole("button", {
name: labels.addToUserList,
})
await user.click(addToUserListButton)
const dialog = screen.getByRole("dialog")
expect(dialog).toBeVisible()
expect(dialog).toHaveTextContent("Sign Up")
})

test("Clicking card opens resource drawer", async () => {
const { resource, location } = setup({
user: { is_learning_path_editor: true },
})
invariant(resource)
const cardTitle = screen.getByRole("heading", { name: resource.title })
await user.click(cardTitle)
expect(
new URLSearchParams(location.current.search).get(
RESOURCE_DRAWER_QUERY_PARAM,
),
).toBe(String(resource.id))
})
})
132 changes: 132 additions & 0 deletions frontends/mit-open/src/page-components/ResourceCard/ResourceCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useCallback, useMemo, useState } from "react"
import { LearningResourceCard, LearningResourceListCard } from "ol-components"
import * as NiceModal from "@ebay/nice-modal-react"
import type {
LearningResourceCardProps,
LearningResourceListCardProps,
} from "ol-components"
import {
AddToLearningPathDialog,
AddToUserListDialog,
} from "../Dialogs/AddToListDialog"
import { useResourceDrawerHref } from "../LearningResourceDrawer/LearningResourceDrawer"
import { useUserMe } from "api/hooks/user"
import { SignupPopover } from "../SignupPopover/SignupPopover"

const useResourceCard = () => {
const getDrawerHref = useResourceDrawerHref()
const { data: user } = useUserMe()

const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)

const handleClosePopover = useCallback(() => {
setAnchorEl(null)
}, [])
const handleAddToLearningPathClick: LearningResourceCardProps["onAddToLearningPathClick"] =
useMemo(() => {
if (user?.is_authenticated && user?.is_learning_path_editor) {
return (event, resourceId: number) => {
NiceModal.show(AddToLearningPathDialog, { resourceId })
}
}
return null
}, [user])

const handleAddToUserListClick: LearningResourceCardProps["onAddToUserListClick"] =
useMemo(() => {
if (!user) {
// user info is still loading
return null
}
if (user.is_authenticated) {
return (event, resourceId: number) => {
NiceModal.show(AddToUserListDialog, { resourceId })
}
}
return (event) => {
setAnchorEl(event.currentTarget)
}
}, [user])

return {
getDrawerHref,
anchorEl,
handleClosePopover,
handleAddToLearningPathClick,
handleAddToUserListClick,
}
}

type ResourceCardProps = Omit<
LearningResourceCardProps,
"href" | "onAddToLearningPathClick" | "onAddToUserListClick"
>

/**
* Just like `ol-components/LearningResourceCard`, but with builtin actions:
* - click opens the Resource Drawer
* - onAddToListClick opens the Add to List modal
* - for unauthenticated users, a popover prompts signup instead.
* - onAddToLearningPathClick opens the Add to Learning Path modal
*/
const ResourceCard: React.FC<ResourceCardProps> = ({ resource, ...others }) => {
const {
getDrawerHref,
anchorEl,
handleClosePopover,
handleAddToLearningPathClick,
handleAddToUserListClick,
} = useResourceCard()
return (
<>
<LearningResourceCard
resource={resource}
href={resource ? getDrawerHref(resource.id) : undefined}
onAddToLearningPathClick={handleAddToLearningPathClick}
onAddToUserListClick={handleAddToUserListClick}
{...others}
/>
<SignupPopover anchorEl={anchorEl} onClose={handleClosePopover} />
</>
)
}

type ResourceListCardProps = Omit<
LearningResourceListCardProps,
"href" | "onAddToLearningPathClick" | "onAddToUserListClick"
>

/**
* Just like `ol-components/LearningResourceListCard`, but with builtin actions:
* - click opens the Resource Drawer
* - onAddToListClick opens the Add to List modal
* - for unauthenticated users, a popover prompts signup instead.
* - onAddToLearningPathClick opens the Add to Learning Path modal
*/
const ResourceListCard: React.FC<ResourceListCardProps> = ({
resource,
...others
}) => {
const {
getDrawerHref,
anchorEl,
handleClosePopover,
handleAddToLearningPathClick,
handleAddToUserListClick,
} = useResourceCard()
return (
<>
<LearningResourceListCard
resource={resource}
href={resource ? getDrawerHref(resource.id) : undefined}
onAddToLearningPathClick={handleAddToLearningPathClick}
onAddToUserListClick={handleAddToUserListClick}
{...others}
/>
<SignupPopover anchorEl={anchorEl} onClose={handleClosePopover} />
</>
)
}

export { ResourceCard, ResourceListCard }
export type { ResourceCardProps, ResourceListCardProps }
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { factories, setMockResponse, makeRequest, urls } from "api/test-utils"
import { LearningResourceCard } from "ol-components"
import { ControlledPromise } from "ol-test-utilities"
import invariant from "tiny-invariant"

jest.mock("ol-components", () => {
const actual = jest.requireActual("ol-components")
Expand Down Expand Up @@ -161,9 +162,17 @@ describe("ResourceCarousel", () => {
<ResourceCarousel title="My Carousel" config={config} />,
)
await waitFor(() => {
expect(makeRequest.mock.calls.length > 0).toBe(true)
expect(makeRequest).toHaveBeenCalledWith(
"get",
expect.stringContaining(urls.learningResources.list()),
undefined,
)
})
const [_method, url] = makeRequest.mock.calls[0]
const [_method, url] =
makeRequest.mock.calls.find(([_method, url]) => {
return url.includes(urls.learningResources.list())
}) ?? []
invariant(url)
const urlParams = new URLSearchParams(url.split("?")[1])
expect(urlParams.getAll("resource_type")).toEqual(["course", "program"])
expect(urlParams.get("professional")).toEqual("true")
Expand Down
Loading