Skip to content

Commit 259b2b0

Browse files
authored
feat: add neighborhood amenities as dropdowns (#5553)
* feat: add neighborhood amenities as dropdowns * fix: switch array to enum * test: add test for NeighborhoodAmenities partners section * test: add options check for select
1 parent b761b62 commit 259b2b0

6 files changed

Lines changed: 373 additions & 10 deletions

File tree

api/prisma/seed-staging.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,11 @@ export const stagingSeed = async (
157157
});
158158
const angelopolisJurisdiction = await prismaClient.jurisdictions.create({
159159
data: jurisdictionFactory('Angelopolis', {
160-
featureFlags: [FeatureFlagEnum.enableNeighborhoodAmenities, FeatureFlagEnum.enableHousingDeveloperOwner],
160+
featureFlags: [
161+
FeatureFlagEnum.enableHousingDeveloperOwner,
162+
FeatureFlagEnum.enableNeighborhoodAmenities,
163+
FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown,
164+
],
161165
visibleNeighborhoodAmenities: [
162166
NeighborhoodAmenitiesEnum.groceryStores,
163167
NeighborhoodAmenitiesEnum.pharmacies,

api/src/enums/feature-flags/feature-flags-enum.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export enum FeatureFlagEnum {
2424
enableListingUpdatedAt = 'enableListingUpdatedAt',
2525
enableMarketingStatus = 'enableMarketingStatus',
2626
enableNeighborhoodAmenities = 'enableNeighborhoodAmenities',
27+
enableNeighborhoodAmenitiesDropdown = 'enableNeighborhoodAmenitiesDropdown',
2728
enableNonRegulatedListings = 'enableNonRegulatedListings',
2829
enablePartnerDemographics = 'enablePartnerDemographics',
2930
enablePartnerSettings = 'enablePartnerSettings',
@@ -154,6 +155,11 @@ export const featureFlagMap: { name: string; description: string }[] = [
154155
description:
155156
"When true, the 'neighborhood amenities' section is displayed in listing creation/edit and the public listing view",
156157
},
158+
{
159+
name: FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown,
160+
description:
161+
'When true, neighborhood amenities inputs render as dropdowns with distance options instead of textareas',
162+
},
157163
{
158164
name: FeatureFlagEnum.enableNonRegulatedListings,
159165
description:

shared-helpers/src/types/backend-swagger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7582,6 +7582,7 @@ export enum FeatureFlagEnum {
75827582
"enableListingUpdatedAt" = "enableListingUpdatedAt",
75837583
"enableMarketingStatus" = "enableMarketingStatus",
75847584
"enableNeighborhoodAmenities" = "enableNeighborhoodAmenities",
7585+
"enableNeighborhoodAmenitiesDropdown" = "enableNeighborhoodAmenitiesDropdown",
75857586
"enableNonRegulatedListings" = "enableNonRegulatedListings",
75867587
"enablePartnerDemographics" = "enablePartnerDemographics",
75877588
"enablePartnerSettings" = "enablePartnerSettings",
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import React from "react"
2+
import { screen, waitFor, within } from "@testing-library/react"
3+
import { FormProvider, useForm } from "react-hook-form"
4+
import { AuthContext } from "@bloom-housing/shared-helpers"
5+
import {
6+
FeatureFlagEnum,
7+
Jurisdiction,
8+
JurisdictionsService,
9+
NeighborhoodAmenitiesEnum,
10+
} from "@bloom-housing/shared-helpers/src/types/backend-swagger"
11+
import { t } from "@bloom-housing/ui-components"
12+
import { mockNextRouter, render } from "../../../../testUtils"
13+
import { formDefaults, FormListing } from "../../../../../src/lib/listings/formTypes"
14+
import NeighborhoodAmenities from "../../../../../src/components/listings/PaperListingForm/sections/NeighborhoodAmenities"
15+
16+
const FormComponent = ({ children, values }: { values?: FormListing; children }) => {
17+
const formMethods = useForm<FormListing>({
18+
defaultValues: { ...formDefaults, ...values },
19+
shouldUnregister: false,
20+
})
21+
return <FormProvider {...formMethods}>{children}</FormProvider>
22+
}
23+
24+
beforeAll(() => {
25+
mockNextRouter()
26+
})
27+
28+
describe("NeighborhoodAmenities", () => {
29+
const mockJurisdictionWithAllAmenities = {
30+
id: "jurisdiction1",
31+
name: "Test Jurisdiction",
32+
visibleNeighborhoodAmenities: [
33+
NeighborhoodAmenitiesEnum.groceryStores,
34+
NeighborhoodAmenitiesEnum.publicTransportation,
35+
NeighborhoodAmenitiesEnum.schools,
36+
NeighborhoodAmenitiesEnum.parksAndCommunityCenters,
37+
NeighborhoodAmenitiesEnum.pharmacies,
38+
NeighborhoodAmenitiesEnum.healthCareResources,
39+
],
40+
}
41+
42+
const mockJurisdictionWithLimitedAmenities = {
43+
id: "jurisdiction2",
44+
name: "Limited Jurisdiction",
45+
visibleNeighborhoodAmenities: [
46+
NeighborhoodAmenitiesEnum.groceryStores,
47+
NeighborhoodAmenitiesEnum.publicTransportation,
48+
],
49+
}
50+
51+
it("should not render when feature flag is disabled", () => {
52+
const doJurisdictionsHaveFeatureFlagOn = () => false
53+
54+
const { container } = render(
55+
<AuthContext.Provider
56+
value={{
57+
doJurisdictionsHaveFeatureFlagOn,
58+
}}
59+
>
60+
<FormComponent
61+
values={{ ...formDefaults, jurisdictions: { id: "jurisdiction1" } as Jurisdiction }}
62+
>
63+
<NeighborhoodAmenities />
64+
</FormComponent>
65+
</AuthContext.Provider>
66+
)
67+
68+
expect(container.firstChild).toBeNull()
69+
})
70+
71+
it("should not render when no jurisdiction is selected", () => {
72+
const doJurisdictionsHaveFeatureFlagOn = () => true
73+
74+
const { container } = render(
75+
<AuthContext.Provider
76+
value={{
77+
doJurisdictionsHaveFeatureFlagOn,
78+
}}
79+
>
80+
<FormComponent>
81+
<NeighborhoodAmenities />
82+
</FormComponent>
83+
</AuthContext.Provider>
84+
)
85+
86+
expect(container.firstChild).toBeNull()
87+
})
88+
89+
it("should render all neighborhood amenities as textareas when dropdown is disabled", async () => {
90+
const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithAllAmenities)
91+
92+
const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => {
93+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true
94+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return false
95+
return false
96+
}
97+
98+
render(
99+
<AuthContext.Provider
100+
value={{
101+
doJurisdictionsHaveFeatureFlagOn,
102+
jurisdictionsService: {
103+
retrieve: mockRetrieve,
104+
} as unknown as JurisdictionsService,
105+
}}
106+
>
107+
<FormComponent
108+
values={{ ...formDefaults, jurisdictions: { id: "jurisdiction1" } as Jurisdiction }}
109+
>
110+
<NeighborhoodAmenities />
111+
</FormComponent>
112+
</AuthContext.Provider>
113+
)
114+
115+
await screen.findByRole("heading", { name: "Neighborhood amenities" })
116+
expect(
117+
screen.getByText(
118+
"Provide details about any local amenities including grocery stores, health services and parks within 2 miles of your listing."
119+
)
120+
).toBeInTheDocument()
121+
122+
// Test all visible amenities are rendered as textareas
123+
for (const amenity of mockJurisdictionWithAllAmenities.visibleNeighborhoodAmenities) {
124+
const amenityLabel = t(`listings.amenities.${amenity}`)
125+
const textarea = await screen.findByRole("textbox", { name: amenityLabel })
126+
expect(textarea).toBeInTheDocument()
127+
expect(textarea.tagName).toBe("TEXTAREA")
128+
}
129+
})
130+
131+
it("should render neighborhood amenities with dropdowns when dropdown is enabled", async () => {
132+
const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithAllAmenities)
133+
134+
const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => {
135+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true
136+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return true
137+
return false
138+
}
139+
140+
render(
141+
<AuthContext.Provider
142+
value={{
143+
doJurisdictionsHaveFeatureFlagOn,
144+
jurisdictionsService: {
145+
retrieve: mockRetrieve,
146+
} as unknown as JurisdictionsService,
147+
}}
148+
>
149+
<FormComponent
150+
values={{
151+
...formDefaults,
152+
jurisdictions: { id: "jurisdiction1" } as Jurisdiction,
153+
}}
154+
>
155+
<NeighborhoodAmenities />
156+
</FormComponent>
157+
</AuthContext.Provider>
158+
)
159+
160+
await waitFor(() => {
161+
expect(screen.getByRole("heading", { name: "Neighborhood amenities" })).toBeInTheDocument()
162+
})
163+
expect(
164+
screen.getByText(
165+
"Provide details about any local amenities including grocery stores, health services and parks within 2 miles of your listing."
166+
)
167+
).toBeInTheDocument()
168+
169+
// Test all visible amenities are rendered as SELECT dropdowns
170+
for (const amenity of mockJurisdictionWithAllAmenities.visibleNeighborhoodAmenities) {
171+
const amenityLabel = t(`listings.amenities.${amenity}`)
172+
const select = screen.getByRole("combobox", { name: amenityLabel })
173+
expect(select).toBeInTheDocument()
174+
expect(select.tagName).toBe("SELECT")
175+
}
176+
})
177+
178+
it("should only render visible amenities from jurisdiction configuration", async () => {
179+
const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities)
180+
181+
const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => {
182+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true
183+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return false
184+
return false
185+
}
186+
187+
render(
188+
<AuthContext.Provider
189+
value={{
190+
doJurisdictionsHaveFeatureFlagOn,
191+
jurisdictionsService: {
192+
retrieve: mockRetrieve,
193+
} as unknown as JurisdictionsService,
194+
}}
195+
>
196+
<FormComponent
197+
values={{
198+
...formDefaults,
199+
jurisdictions: { id: "jurisdiction2" } as Jurisdiction,
200+
}}
201+
>
202+
<NeighborhoodAmenities />
203+
</FormComponent>
204+
</AuthContext.Provider>
205+
)
206+
207+
await screen.findByRole("heading", { name: "Neighborhood amenities" })
208+
209+
expect(screen.getByRole("textbox", { name: "Grocery stores" })).toBeInTheDocument()
210+
expect(screen.getByRole("textbox", { name: "Public transportation" })).toBeInTheDocument()
211+
212+
expect(screen.queryByRole("textbox", { name: "Schools" })).not.toBeInTheDocument()
213+
expect(
214+
screen.queryByRole("textbox", { name: "Parks and community centers" })
215+
).not.toBeInTheDocument()
216+
expect(screen.queryByRole("textbox", { name: "Pharmacies" })).not.toBeInTheDocument()
217+
expect(screen.queryByRole("textbox", { name: "Health care resources" })).not.toBeInTheDocument()
218+
})
219+
220+
it("should include distance options in dropdown when enabled", async () => {
221+
const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities)
222+
223+
const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => {
224+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true
225+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return true
226+
return false
227+
}
228+
229+
render(
230+
<AuthContext.Provider
231+
value={{
232+
doJurisdictionsHaveFeatureFlagOn,
233+
jurisdictionsService: {
234+
retrieve: mockRetrieve,
235+
} as unknown as JurisdictionsService,
236+
}}
237+
>
238+
<FormComponent
239+
values={{ ...formDefaults, jurisdictions: { id: "jurisdiction1" } as Jurisdiction }}
240+
>
241+
<NeighborhoodAmenities />
242+
</FormComponent>
243+
</AuthContext.Provider>
244+
)
245+
246+
const select = await screen.findByRole("combobox", { name: "Grocery stores" })
247+
expect(select).toBeInTheDocument()
248+
249+
const options = within(select).getAllByRole("option")
250+
251+
const optionTexts = options.map((option) => option.textContent)
252+
const expectedDistanceOptions = [
253+
"On site",
254+
"One block",
255+
"Two blocks",
256+
"Three blocks",
257+
"Four blocks",
258+
"Five blocks",
259+
"Within one mile",
260+
"Within two miles",
261+
"Within three miles",
262+
"Within four miles",
263+
]
264+
expect(optionTexts).toContain("Select one")
265+
expect(optionTexts.length).toBe(expectedDistanceOptions.length + 1)
266+
267+
for (const distanceOption of expectedDistanceOptions) {
268+
expect(optionTexts).toContain(distanceOption)
269+
}
270+
})
271+
272+
it("should render partial amenities in 1 row when there are less or equal to 2 amenities", async () => {
273+
const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities)
274+
275+
const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => {
276+
if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true
277+
return false
278+
}
279+
280+
const { container } = render(
281+
<AuthContext.Provider
282+
value={{
283+
doJurisdictionsHaveFeatureFlagOn,
284+
jurisdictionsService: {
285+
retrieve: mockRetrieve,
286+
} as unknown as JurisdictionsService,
287+
}}
288+
>
289+
<FormComponent
290+
values={{ ...formDefaults, jurisdictions: { id: "jurisdiction2" } as Jurisdiction }}
291+
>
292+
<NeighborhoodAmenities />
293+
</FormComponent>
294+
</AuthContext.Provider>
295+
)
296+
297+
await screen.findByRole("heading", { name: "Neighborhood amenities" })
298+
299+
// Check that Grid.Row elements are created (2 amenities = 1 row)
300+
const rows = container.querySelectorAll('[class*="grid-row"]')
301+
expect(rows.length).toBe(1)
302+
})
303+
})

sites/partners/page_content/locale_overrides/general.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,16 @@
508508
"nav.flags": "Flags",
509509
"nav.siteTitlePartners": "Partners Portal",
510510
"nav.users": "Users",
511+
"neighborhoodAmenities.distance.onSite": "On site",
512+
"neighborhoodAmenities.distance.oneBlock": "One block",
513+
"neighborhoodAmenities.distance.twoBlocks": "Two blocks",
514+
"neighborhoodAmenities.distance.threeBlocks": "Three blocks",
515+
"neighborhoodAmenities.distance.fourBlocks": "Four blocks",
516+
"neighborhoodAmenities.distance.fiveBlocks": "Five blocks",
517+
"neighborhoodAmenities.distance.withinOneMile": "Within one mile",
518+
"neighborhoodAmenities.distance.withinTwoMiles": "Within two miles",
519+
"neighborhoodAmenities.distance.withinThreeMiles": "Within three miles",
520+
"neighborhoodAmenities.distance.withinFourMiles": "Within four miles",
511521
"settings.createCopy": "Make a copy",
512522
"settings.createCopyDescription": "Create a copy of your preference.",
513523
"settings.preference": "Preference",

0 commit comments

Comments
 (0)