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
4 changes: 2 additions & 2 deletions .github/workflows/repo-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:
./node_modules/.bin/firebase --project demo-dtp emulators:exec
--only auth,firestore
--import tests/integration/exportedTestData
"yarn firebase-admin run-script backfillTestimonyBallotQuestionId --env local"
"yarn firebase-admin run-script backfillTestimonyScope --env local"

- name: Run Integration Tests
run: >
Expand All @@ -87,4 +87,4 @@ jobs:
tests/integration/moderation.test.ts
tests/integration/profile.test.ts
tests/integration/ballotQuestions.test.ts
tests/integration/backfillTestimonyBallotQuestionId.test.ts
tests/integration/backfillTestimonyScope.test.ts
1 change: 1 addition & 0 deletions components/Newsfeed/Newsfeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export default function Newsfeed() {
authorUid={element.authorUid}
billId={element.billId}
bodyText={element.bodyText}
conferenceId={element.conferenceId}
court={element.court}
header={element.header}
isBillMatch={element.isBillMatch}
Expand Down
2 changes: 2 additions & 0 deletions components/Newsfeed/NotificationProps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type NotificationProps = {
ballotQuestionId?: string
ballotQuestionCourt?: number
ballotStatus?: string
// Set on testimony to a committee of conference
conferenceId?: string | null
}

export type Notifications = NotificationProps[]
69 changes: 69 additions & 0 deletions components/NewsfeedCard/NewsfeedCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ jest.mock("next-i18next", () => ({
}))

jest.mock("components/links", () => ({
maple: jest.requireActual("components/links").maple,
Internal: ({
href,
children,
Expand All @@ -24,6 +25,15 @@ jest.mock("components/links", () => ({
)
}))

// jsdom has no matchMedia; the testimony bodies ask it for the phone layout
beforeAll(() => {
window.matchMedia = jest.fn().mockReturnValue({
matches: false,
addEventListener: jest.fn(),
removeEventListener: jest.fn()
})
})

describe("NewsfeedCard", () => {
it("renders ballot-question updates with the follow reason and destination link", () => {
render(
Expand All @@ -48,4 +58,63 @@ describe("NewsfeedCard", () => {
screen.getByRole("link", { name: "View ballot question" })
).toHaveAttribute("href", "/ballotQuestions/25-14")
})

const testimony = {
authorUid: "author1",
billId: "S2581",
bodyText: "Keep the Senate's phone rules.",
court: "194",
header: "An Act relative to student learning",
isBillMatch: false,
isUserMatch: true,
subheader: "Alice",
testimonyId: "t1",
timestamp: Timestamp.fromDate(new Date("2025-03-01")),
type: "testimony",
userRole: "user"
}

it("leads conference testimony to its conference with the stance in words", () => {
render(
<NewsfeedCard
{...testimony}
conferenceId="student-learning-and-mental-health"
position="passSenateVersion"
/>
)

expect(screen.getByText("newsfeed.conferenceInput")).toBeInTheDocument()
expect(
screen.getByText("input.position.passSenateVersion.long")
).toBeInTheDocument()
expect(screen.getByRole("link", { name: "S.2581" })).toHaveAttribute(
"href",
"/conferences/194/student-learning-and-mental-health"
)
expect(
screen.getByRole("link", { name: "newsfeed.viewConferenceInput" })
).toHaveAttribute(
"href",
"/conferences/194/student-learning-and-mental-health#input"
)
expect(document.querySelector("img[src^='/thumbs-']")).toBeNull()
expect(document.querySelector("a[href^='/bills/']")).toBeNull()
expect(document.querySelector("a[href^='/testimony/']")).toBeNull()
})

it("keeps bill testimony on its bill, testimony page and thumbs", () => {
render(<NewsfeedCard {...testimony} position="endorse" />)

expect(screen.getByText("newsfeed.endorsed")).toBeInTheDocument()
expect(screen.getByRole("link", { name: "S.2581" })).toHaveAttribute(
"href",
"/bills/194/S2581"
)
expect(
screen.getByRole("link", { name: "newsfeed.viewFullTestimony" })
).toHaveAttribute("href", "/testimony/t1")
expect(
document.querySelector("img[src='/thumbs-endorse.svg']")
).not.toBeNull()
})
})
19 changes: 18 additions & 1 deletion components/NewsfeedCard/NewsfeedCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import { flags } from "../featureFlags"
import {
NewsfeedBallotQuestionCardBody,
NewsfeedBillCardBody,
NewsfeedConferenceTestimonyCardBody,
NewsfeedTestimonyCardBody
} from "./NewsfeedCardBody"
import NewsfeedCardTitle from "./NewsfeedCardTitle"
import { maple } from "components/links"

const Container = styled.div`
max-width: 700px;
Expand All @@ -19,6 +21,7 @@ export const NewsfeedCard = (props: {
ballotStatus?: string
billId?: string
bodyText: string
conferenceId?: string | null
court?: string
header: string
isBallotQuestionMatch?: boolean
Expand All @@ -34,10 +37,14 @@ export const NewsfeedCard = (props: {
}) => {
const date = props.timestamp.toDate()
const formattedTimestamp = `${date.toLocaleDateString()}`
const conferenceHref = props.conferenceId
? maple.conference({ court: Number(props.court), id: props.conferenceId })
: undefined
const header = (
<NewsfeedCardTitle
authorUid={props.authorUid}
billId={props.billId}
conferenceHref={conferenceHref}
court={props.court}
header={props.header}
isBallotQuestionMatch={props.isBallotQuestionMatch}
Expand All @@ -59,7 +66,17 @@ export const NewsfeedCard = (props: {
/>
)

if (props.type == `testimony`) {
if (props.type == `testimony` && conferenceHref) {
body = (
<NewsfeedConferenceTestimonyCardBody
billText={props.header}
conferenceHref={conferenceHref}
position={props.position}
text={props.bodyText}
timestamp={formattedTimestamp}
/>
)
} else if (props.type == `testimony`) {
body = (
<NewsfeedTestimonyCardBody
billText={props.header}
Expand Down
43 changes: 43 additions & 0 deletions components/NewsfeedCard/NewsfeedCardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useMediaQuery } from "usehooks-ts"
import { Col, Row } from "../bootstrap"
import { Internal } from "components/links"
import { truncateText } from "components/formatting"
import { ConferencePosition } from "components/db"
import { PositionChip } from "components/conference/input/Entry"

export const NewsfeedBallotQuestionCardBody = ({
ballotQuestionId,
Expand Down Expand Up @@ -194,6 +196,47 @@ export const NewsfeedTestimonyCardBody = (props: NewsfeedCardBodyProps) => {
)
}

/** Testimony on a committee of conference: the stance is the conference
* page's chip, and the card leads to the conference, not the bill. */
export const NewsfeedConferenceTestimonyCardBody = (props: {
billText?: string
conferenceHref: string
position?: string
text?: string
timestamp?: string
}) => {
const { billText, conferenceHref, text, timestamp } = props
const { t } = useTranslation("common")

return (
<div>
<CardBootstrap.Body className={`p-2`}>
<CardBootstrap.Text className={`text-secondary mb-2`}>
<strong>{billText}</strong>
</CardBootstrap.Text>
<div className={`bg-body mx-0 mt-1 p-3 rounded`}>
{ConferencePosition.guard(props.position) && (
<PositionChip position={props.position} long />
)}
<strong className={`d-block mt-2`}>{`"${truncateText(
text,
600
)}"`}</strong>
</div>
<div className={`d-flex flex-wrap justify-content-between mt-2 mb-2`}>
<div>
{t("newsfeed.conferenceInputPost")}
{timestamp}
</div>
<Internal href={`${conferenceHref}#input`}>
<div>{t("newsfeed.viewConferenceInput")}</div>
</Internal>
</div>
</CardBootstrap.Body>
</div>
)
}

const NewsfeedTestimonyCardTail = (props: NewsfeedCardBodyProps) => {
const { testimonyId, timestamp } = props
const { t } = useTranslation("common")
Expand Down
15 changes: 12 additions & 3 deletions components/NewsfeedCard/NewsfeedCardTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { Internal } from "components/links"
interface NewsfeedCardTitleProps {
authorUid?: string
billId?: string
/** Set on testimony to a committee of conference: its conference page */
conferenceHref?: string
court?: string
header?: string
subheader?: string
Expand All @@ -25,6 +27,7 @@ export const NewsfeedCardTitle = (props: NewsfeedCardTitleProps) => {
const {
authorUid,
billId,
conferenceHref,
court,
header,
isBillMatch,
Expand All @@ -43,6 +46,7 @@ export const NewsfeedCardTitle = (props: NewsfeedCardTitleProps) => {
<CardTitleHeadline
authorUid={authorUid}
billId={billId}
conferenceHref={conferenceHref}
court={court}
header={header}
subheader={subheader}
Expand Down Expand Up @@ -98,7 +102,8 @@ const CardHeaderImg = (props: NewsfeedCardTitleProps) => {
}

const CardTitleHeadline = (props: NewsfeedCardTitleProps) => {
const { authorUid, billId, court, header, subheader, type } = props
const { authorUid, billId, conferenceHref, court, header, subheader, type } =
props
const { t } = useTranslation("common")

switch (type) {
Expand All @@ -113,8 +118,12 @@ const CardTitleHeadline = (props: NewsfeedCardTitleProps) => {
<strong>{subheader}</strong>
</Internal>

{t("newsfeed.endorsed")}
<a href={`/bills/${court}/${billId}`}>
{t(
conferenceHref
? "newsfeed.conferenceInput"
: "newsfeed.endorsed"
)}
<a href={conferenceHref ?? `/bills/${court}/${billId}`}>
{billId && <strong>{formatBillId(billId)}</strong>}
</a>
</CardBootstrap.Title>
Expand Down
18 changes: 13 additions & 5 deletions components/TestimonyCard/ReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Props = {

additionalInformationLabel: string
requireAdditionalInformation?: boolean
title?: string
children?: string | React.ReactNode
}

Expand All @@ -22,6 +23,7 @@ export function ReportModal({
isLoading,
additionalInformationLabel,
requireAdditionalInformation = false,
title,
children
}: Props) {
const [selectedReason, setSelectedReason] = useState<string | null>(null)
Expand All @@ -46,7 +48,7 @@ export function ReportModal({
return (
<Modal show onHide={onClose}>
<Form validated={false} onSubmit={handleReport}>
<Modal.Header closeButton>{t("reportTestimony")}</Modal.Header>
<Modal.Header closeButton>{title ?? t("reportTestimony")}</Modal.Header>
<Modal.Body>
{children}
<Form.Group as="fieldset">
Expand Down Expand Up @@ -117,8 +119,13 @@ export function ReportModal({
export function RequestDeleteOwnTestimonyModal({
onClose,
onReport,
isLoading
}: Pick<Props, "onClose" | "onReport" | "isLoading">) {
isLoading,
title,
wrongTarget,
children
}: Pick<Props, "onClose" | "onReport" | "isLoading" | "title" | "children"> & {
wrongTarget?: string
}) {
const { t } = useTranslation("testimony", {
keyPrefix: "reportModal"
})
Expand All @@ -127,16 +134,17 @@ export function RequestDeleteOwnTestimonyModal({
onClose={onClose}
onReport={onReport}
isLoading={isLoading}
title={title}
reasons={[
t("wrongBill"),
wrongTarget ?? t("wrongBill"),
t("sensitiveInformation"),
t("userBelow18YearsOld"),
t("other")
]}
additionalInformationLabel={t("reason")}
requireAdditionalInformation
>
{t("rescind")}
{children ?? t("rescind")}
</ReportModal>
)
}
Loading
Loading