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: 1 addition & 2 deletions components/about/SupportMaple/SupportMaple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactNode } from "react"
import { useTranslation } from "next-i18next"
import styled from "styled-components"
import AboutPagesCard from "../../AboutPagesCard/AboutPagesCard"
import { FEEDBACK_URL } from "../../common"
import { ArrowRightIcon } from "../../learn/icons"
import { Internal } from "../../links"
import LearnBreadcrumb from "../../learn/LearnBreadcrumb"
Expand All @@ -10,8 +11,6 @@ import LearnLayout from "../../learn/LearnLayout"

const DONATE_URL = "https://partnersindemocracy.us/?form=FUNMSSLLQEC"
const GITHUB_URL = "https://github.com/codeforboston/maple/graphs/contributors"
const FEEDBACK_URL =
"https://docs.google.com/forms/d/e/1FAIpQLScU0l0bs-QLdVqIlh6iwi-Y2kOrSv7eqH5h4klhhtNGqsxCSw/viewform?usp=sharing"
const BUG_URL =
"https://docs.google.com/forms/d/e/1FAIpQLSebZTHz0XGR4WgPf10yTPXSOuZK2P41HxK0AFGXM57KbFWuDg/viewform?usp=share_link"

Expand Down
2 changes: 2 additions & 0 deletions components/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export const NEWSLETTER_SIGNUP_URL =
"https://cdn.forms-content.sg-form.com/66e4c174-72e5-11f0-847b-fe4d8dd139cf"
export const TRAINING_CALENDAR_URL =
"https://calendar.google.com/calendar/embed?src=998f62323926f0b0076e7f578d3ca72b1bc94c4efa2f24be57b11f52b1b88595%40group.calendar.google.com&ctz=America%2FNew_York"
export const FEEDBACK_URL =
"https://docs.google.com/forms/d/e/1FAIpQLScU0l0bs-QLdVqIlh6iwi-Y2kOrSv7eqH5h4klhhtNGqsxCSw/viewform?usp=sharing"
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ const page = (conference: ConferencePageProps["conference"]) => ({
conference,
conferences: [phoneFree, privacy],
conferees: [],
hearings: []
hearings: [],
gist: null,
differences: [],
similarities: []
})

describe("moving between conferences", () => {
Expand Down
139 changes: 132 additions & 7 deletions components/conference/ConferenceDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,49 @@ const props: ConferencePageProps = {
eventId: 5710,
startsAt: Date.UTC(2026, 5, 4, 17)
}
],
gist: "This bill regulates how students use technology in schools.",
differences: [
{
heading: "Social media ban",
senate: null,
house: "Regulates social media companies under Chapter 93M.",
notes: { "Chapter 93M": ["Platforms must bar users under 14."] },
source: {
organisation: "Example Gazette",
url: "https://www.example.com/explainer",
publishedAt: "2026-09-01"
}
},
{
heading: "Use or possession",
senate: "Bans use and possession.",
house: "Bans use.",
notes: {},
source: {
organisation: "Other Radio",
url: "https://example.org/report",
publishedAt: null
}
}
],
similarities: [
{
heading: "Phones barred during the school day",
body: "Every district must have a policy.",
source: {
organisation: "Example Gazette",
url: "https://www.example.com/explainer",
publishedAt: "2026-09-01"
}
}
]
}

/** What useWide reads: jsdom lays nothing out, so every element is 0 wide. */
const widen = (px: number) =>
jest.spyOn(HTMLElement.prototype, "clientWidth", "get").mockReturnValue(px)

beforeEach(() => {
mockBill.mockImplementation((_court: number, id: string) => texts[id])
})
Expand Down Expand Up @@ -375,9 +415,10 @@ describe("ConferenceDetails", () => {

it("takes input only while the conference is appointed", () => {
const { unmount } = render(<ConferenceDetails {...props} />)
expect(
screen.getByRole("button", { name: "input.share" })
).toBeInTheDocument()
// The title's, and the one beside the reader's own legislator.
expect(screen.getAllByRole("button", { name: "input.share" })).toHaveLength(
2
)
unmount()
const reported = { ...conference, status: "reportFiled" as const }
render(<ConferenceDetails {...props} conference={reported} />)
Expand All @@ -395,6 +436,7 @@ describe("ConferenceDetails", () => {
.map(a => [a.textContent, a.getAttribute("href")])
expect(tabs).toEqual([
["tab_committee", "#committee"],
["tab_decided", "#decided"],
["tab_input", "#input"],
["tab_text", "#text"]
])
Expand All @@ -406,8 +448,9 @@ describe("ConferenceDetails", () => {
// jsdom lays nothing out, so each section says where it is, 850px tall.
const tops: Record<string, number> = {
committee: 0,
input: 900,
text: 1800
decided: 900,
input: 1800,
text: 2700
}
const rect = jest
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
Expand All @@ -423,19 +466,101 @@ describe("ConferenceDetails", () => {
.map(a => a.textContent)
expect(lit()).toEqual(["tab_committee"])
// The last section lights too, once a jump has parked it.
tops.decided = -1800
tops.input = -900
tops.text = 0
fireEvent.scroll(document)
expect(lit()).toEqual(["tab_text"])
// A section can move with no scroll, when one above it grows: posting
// puts Your Input above Public Input.
tops.input = 900
tops.text = 1800
tops.decided = 900
tops.input = 1800
tops.text = 2700
act(() => resized.forEach(heard => heard()))
expect(lit()).toEqual(["tab_committee"])
rect.mockRestore()
})

describe("what needs to be resolved", () => {
afterEach(() => jest.restoreAllMocks())

it("opens the first difference, each chamber's text in its own card", () => {
render(<ConferenceDetails {...props} />)
const [first, second] = screen
.getAllByRole("button", { expanded: true })
.concat(screen.getAllByRole("button", { expanded: false }))
.filter(b => /Social media ban|Use or possession/.test(b.textContent!))
expect(first).toHaveTextContent("Social media ban")
expect(first).toHaveAttribute("aria-expanded", "true")
expect(second).toHaveAttribute("aria-expanded", "false")
// The Senate bill has nothing on it, so its card says so.
expect(screen.getByText("decided.not_in_bill")).toBeInTheDocument()
expect(screen.getByText(/Regulates social media/)).toBeInTheDocument()
expect(screen.queryByText("Bans use.")).not.toBeInTheDocument()
fireEvent.click(second)
expect(screen.getByText("Bans use.")).toBeInTheDocument()
})

it("opens a term's note on hover", () => {
render(<ConferenceDetails {...props} />)
fireEvent.pointerEnter(screen.getByText("Chapter 93M"))
expect(screen.getByRole("tooltip")).toHaveTextContent(
"Platforms must bar users under 14."
)
})

it("lists the matches beside the differences only when there is room", () => {
const { unmount } = render(<ConferenceDetails {...props} />)
expect(screen.queryByText("decided.match")).not.toBeInTheDocument()
unmount()
widen(800)
render(<ConferenceDetails {...props} />)
const row = screen.getByRole("button", {
name: /Phones barred during the school day/
})
expect(
screen.queryByText("Every district must have a policy.")
).toBeNull()
fireEvent.click(row)
expect(
screen.getByText("Every district must have a policy.")
).toBeInTheDocument()
})

it("numbers each source once and lists it with its host", () => {
widen(800)
render(<ConferenceDetails {...props} />)
const items = within(
screen.getByText("decided.sources").nextElementSibling as HTMLElement
).getAllByRole("listitem")
expect(items).toHaveLength(2)
expect(items[0]).toHaveTextContent("Example Gazette, Sep 1, 2026.")
expect(within(items[0]).getByRole("link")).toHaveTextContent(
"example.com"
)
expect(items[1]).toHaveTextContent("Other Radio.")
// The match cites the first source again rather than a third.
expect(
screen.getByRole("button", { name: /Phones barred/ })
).toHaveTextContent("1")
})

it("says the reader's own legislator is deciding", () => {
render(<ConferenceDetails {...props} />)
expect(screen.getByTestId("decided.yours_one")).toHaveTextContent(
"your_representative A. Peisch"
)
})

it("leaves the section and its tab out where nothing is written", () => {
render(
<ConferenceDetails {...props} differences={[]} similarities={[]} />
)
expect(screen.queryByText("decided.title")).not.toBeInTheDocument()
expect(screen.queryByText("tab_decided")).not.toBeInTheDocument()
})
})

describe("bill text", () => {
it("shows both texts side by side, Senate first, a paragraph per line", () => {
render(<ConferenceDetails {...props} />)
Expand Down
33 changes: 26 additions & 7 deletions components/conference/ConferenceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import { takesTestimony } from "../../functions/src/shared"
import styled from "styled-components"
import { ConferenceByline } from "./ConferenceByline"
import { ConferenceCommittee } from "./ConferenceCommittee"
import { ConferenceDifferences } from "./ConferenceDifferences"
import { ConferenceHero } from "./ConferenceHero"
import { ConferencePicker } from "./ConferencePicker"
import { ConferenceRail } from "./ConferenceRail"
import { ConferenceTabs } from "./ConferenceTabs"
import { ConferenceTabs, TABS_H } from "./ConferenceTabs"
import { ConferenceText } from "./ConferenceText"
import { useAuth } from "../auth"
import { authStepChanged } from "../auth/redux"
Expand Down Expand Up @@ -108,17 +109,19 @@ const TitleBand = styled(Band)`
padding-bottom: 0.75rem;
`

/** A section the tabs lead to, parked under the title and the tabs: 4.5rem is
* ConferenceTabs' row and the mockup's 24px under it. */
/** A section the tabs lead to, parked under the title and the tabs with the
* mockup's 24px under them. */
const Section = styled.div`
margin-top: 1.5rem;
scroll-margin-top: calc(
var(--maple-navbar-height) + var(--title-h, 5rem) + 4.5rem
var(--maple-navbar-height) + var(--title-h, 5rem) + ${TABS_H} + 1.5rem
);
`

/** In the mockup's order, which goes on to the sections still to come. */
const SECTIONS = ["committee", "input", "text"] as const
/** In the mockup's order, which goes on to the sections still to come. A
* conference with nothing written on what is unresolved has no such section. */
const SECTIONS = ["committee", "decided", "input", "text"] as const
const UNDECIDED = SECTIONS.filter(s => s !== "decided")

/**
* One conference: who is deciding, and the two texts they are deciding
Expand All @@ -140,6 +143,9 @@ function Details({
conferences,
conferees,
hearings,
gist,
differences,
similarities,
openOnHero = false
}: ConferencePageProps & { openOnHero?: boolean }) {
const { top, hero, toConference } = useConferenceTop(openOnHero)
Expand All @@ -160,6 +166,7 @@ function Details({
const followed = useFollowedAuthors(user?.uid)
const subject = conferenceName(conference)
const taking = takesTestimony(conference.status)
const decided = differences.length > 0 || similarities.length > 0

const close = useCallback(() => setOpen(false), [])
const edit = useCallback(() => {
Expand Down Expand Up @@ -239,7 +246,7 @@ function Details({
$top="calc(var(--maple-navbar-height) + var(--title-h, 5rem))"
$z={13}
>
<ConferenceTabs sections={SECTIONS} />
<ConferenceTabs sections={decided ? SECTIONS : UNDECIDED} />
</Band>
<Section id="committee">
{/* Keyed so a pinned seat does not outlive its conference. */}
Expand All @@ -250,6 +257,18 @@ function Details({
hearings={hearings}
/>
</Section>
{decided && (
<Section id="decided">
<ConferenceDifferences
gist={gist}
differences={differences}
similarities={similarities}
conferees={conferees}
onShare={taking ? share : undefined}
posted={Boolean(mine)}
/>
</Section>
)}
{mine && (
<Section>
<YourInput testimony={mine} />
Expand Down
Loading
Loading