diff --git a/api/prisma/seed-staging.ts b/api/prisma/seed-staging.ts index b0b16078aa5..b6d40f6d09a 100644 --- a/api/prisma/seed-staging.ts +++ b/api/prisma/seed-staging.ts @@ -157,7 +157,11 @@ export const stagingSeed = async ( }); const angelopolisJurisdiction = await prismaClient.jurisdictions.create({ data: jurisdictionFactory('Angelopolis', { - featureFlags: [FeatureFlagEnum.enableNeighborhoodAmenities, FeatureFlagEnum.enableHousingDeveloperOwner], + featureFlags: [ + FeatureFlagEnum.enableHousingDeveloperOwner, + FeatureFlagEnum.enableNeighborhoodAmenities, + FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown, + ], visibleNeighborhoodAmenities: [ NeighborhoodAmenitiesEnum.groceryStores, NeighborhoodAmenitiesEnum.pharmacies, diff --git a/api/src/enums/feature-flags/feature-flags-enum.ts b/api/src/enums/feature-flags/feature-flags-enum.ts index d4ef3a08b3f..16af2dd738a 100644 --- a/api/src/enums/feature-flags/feature-flags-enum.ts +++ b/api/src/enums/feature-flags/feature-flags-enum.ts @@ -24,6 +24,7 @@ export enum FeatureFlagEnum { enableListingUpdatedAt = 'enableListingUpdatedAt', enableMarketingStatus = 'enableMarketingStatus', enableNeighborhoodAmenities = 'enableNeighborhoodAmenities', + enableNeighborhoodAmenitiesDropdown = 'enableNeighborhoodAmenitiesDropdown', enableNonRegulatedListings = 'enableNonRegulatedListings', enablePartnerDemographics = 'enablePartnerDemographics', enablePartnerSettings = 'enablePartnerSettings', @@ -154,6 +155,11 @@ export const featureFlagMap: { name: string; description: string }[] = [ description: "When true, the 'neighborhood amenities' section is displayed in listing creation/edit and the public listing view", }, + { + name: FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown, + description: + 'When true, neighborhood amenities inputs render as dropdowns with distance options instead of textareas', + }, { name: FeatureFlagEnum.enableNonRegulatedListings, description: diff --git a/shared-helpers/src/types/backend-swagger.ts b/shared-helpers/src/types/backend-swagger.ts index f23c42e535f..3c48427d172 100644 --- a/shared-helpers/src/types/backend-swagger.ts +++ b/shared-helpers/src/types/backend-swagger.ts @@ -7582,6 +7582,7 @@ export enum FeatureFlagEnum { "enableListingUpdatedAt" = "enableListingUpdatedAt", "enableMarketingStatus" = "enableMarketingStatus", "enableNeighborhoodAmenities" = "enableNeighborhoodAmenities", + "enableNeighborhoodAmenitiesDropdown" = "enableNeighborhoodAmenitiesDropdown", "enableNonRegulatedListings" = "enableNonRegulatedListings", "enablePartnerDemographics" = "enablePartnerDemographics", "enablePartnerSettings" = "enablePartnerSettings", diff --git a/sites/partners/__tests__/components/listings/PaperListingForm/sections/NeighborhoodAmenities.test.tsx b/sites/partners/__tests__/components/listings/PaperListingForm/sections/NeighborhoodAmenities.test.tsx new file mode 100644 index 00000000000..456b6a2145f --- /dev/null +++ b/sites/partners/__tests__/components/listings/PaperListingForm/sections/NeighborhoodAmenities.test.tsx @@ -0,0 +1,303 @@ +import React from "react" +import { screen, waitFor, within } from "@testing-library/react" +import { FormProvider, useForm } from "react-hook-form" +import { AuthContext } from "@bloom-housing/shared-helpers" +import { + FeatureFlagEnum, + Jurisdiction, + JurisdictionsService, + NeighborhoodAmenitiesEnum, +} from "@bloom-housing/shared-helpers/src/types/backend-swagger" +import { t } from "@bloom-housing/ui-components" +import { mockNextRouter, render } from "../../../../testUtils" +import { formDefaults, FormListing } from "../../../../../src/lib/listings/formTypes" +import NeighborhoodAmenities from "../../../../../src/components/listings/PaperListingForm/sections/NeighborhoodAmenities" + +const FormComponent = ({ children, values }: { values?: FormListing; children }) => { + const formMethods = useForm({ + defaultValues: { ...formDefaults, ...values }, + shouldUnregister: false, + }) + return {children} +} + +beforeAll(() => { + mockNextRouter() +}) + +describe("NeighborhoodAmenities", () => { + const mockJurisdictionWithAllAmenities = { + id: "jurisdiction1", + name: "Test Jurisdiction", + visibleNeighborhoodAmenities: [ + NeighborhoodAmenitiesEnum.groceryStores, + NeighborhoodAmenitiesEnum.publicTransportation, + NeighborhoodAmenitiesEnum.schools, + NeighborhoodAmenitiesEnum.parksAndCommunityCenters, + NeighborhoodAmenitiesEnum.pharmacies, + NeighborhoodAmenitiesEnum.healthCareResources, + ], + } + + const mockJurisdictionWithLimitedAmenities = { + id: "jurisdiction2", + name: "Limited Jurisdiction", + visibleNeighborhoodAmenities: [ + NeighborhoodAmenitiesEnum.groceryStores, + NeighborhoodAmenitiesEnum.publicTransportation, + ], + } + + it("should not render when feature flag is disabled", () => { + const doJurisdictionsHaveFeatureFlagOn = () => false + + const { container } = render( + + + + + + ) + + expect(container.firstChild).toBeNull() + }) + + it("should not render when no jurisdiction is selected", () => { + const doJurisdictionsHaveFeatureFlagOn = () => true + + const { container } = render( + + + + + + ) + + expect(container.firstChild).toBeNull() + }) + + it("should render all neighborhood amenities as textareas when dropdown is disabled", async () => { + const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithAllAmenities) + + const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => { + if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true + if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return false + return false + } + + render( + + + + + + ) + + await screen.findByRole("heading", { name: "Neighborhood amenities" }) + expect( + screen.getByText( + "Provide details about any local amenities including grocery stores, health services and parks within 2 miles of your listing." + ) + ).toBeInTheDocument() + + // Test all visible amenities are rendered as textareas + for (const amenity of mockJurisdictionWithAllAmenities.visibleNeighborhoodAmenities) { + const amenityLabel = t(`listings.amenities.${amenity}`) + const textarea = await screen.findByRole("textbox", { name: amenityLabel }) + expect(textarea).toBeInTheDocument() + expect(textarea.tagName).toBe("TEXTAREA") + } + }) + + it("should render neighborhood amenities with dropdowns when dropdown is enabled", async () => { + const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithAllAmenities) + + const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => { + if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true + if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return true + return false + } + + render( + + + + + + ) + + await waitFor(() => { + expect(screen.getByRole("heading", { name: "Neighborhood amenities" })).toBeInTheDocument() + }) + expect( + screen.getByText( + "Provide details about any local amenities including grocery stores, health services and parks within 2 miles of your listing." + ) + ).toBeInTheDocument() + + // Test all visible amenities are rendered as SELECT dropdowns + for (const amenity of mockJurisdictionWithAllAmenities.visibleNeighborhoodAmenities) { + const amenityLabel = t(`listings.amenities.${amenity}`) + const select = screen.getByRole("combobox", { name: amenityLabel }) + expect(select).toBeInTheDocument() + expect(select.tagName).toBe("SELECT") + } + }) + + it("should only render visible amenities from jurisdiction configuration", async () => { + const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities) + + const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => { + if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true + if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return false + return false + } + + render( + + + + + + ) + + await screen.findByRole("heading", { name: "Neighborhood amenities" }) + + expect(screen.getByRole("textbox", { name: "Grocery stores" })).toBeInTheDocument() + expect(screen.getByRole("textbox", { name: "Public transportation" })).toBeInTheDocument() + + expect(screen.queryByRole("textbox", { name: "Schools" })).not.toBeInTheDocument() + expect( + screen.queryByRole("textbox", { name: "Parks and community centers" }) + ).not.toBeInTheDocument() + expect(screen.queryByRole("textbox", { name: "Pharmacies" })).not.toBeInTheDocument() + expect(screen.queryByRole("textbox", { name: "Health care resources" })).not.toBeInTheDocument() + }) + + it("should include distance options in dropdown when enabled", async () => { + const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities) + + const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => { + if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true + if (flag === FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown) return true + return false + } + + render( + + + + + + ) + + const select = await screen.findByRole("combobox", { name: "Grocery stores" }) + expect(select).toBeInTheDocument() + + const options = within(select).getAllByRole("option") + + const optionTexts = options.map((option) => option.textContent) + const expectedDistanceOptions = [ + "On site", + "One block", + "Two blocks", + "Three blocks", + "Four blocks", + "Five blocks", + "Within one mile", + "Within two miles", + "Within three miles", + "Within four miles", + ] + expect(optionTexts).toContain("Select one") + expect(optionTexts.length).toBe(expectedDistanceOptions.length + 1) + + for (const distanceOption of expectedDistanceOptions) { + expect(optionTexts).toContain(distanceOption) + } + }) + + it("should render partial amenities in 1 row when there are less or equal to 2 amenities", async () => { + const mockRetrieve = jest.fn().mockResolvedValue(mockJurisdictionWithLimitedAmenities) + + const doJurisdictionsHaveFeatureFlagOn = (flag: FeatureFlagEnum) => { + if (flag === FeatureFlagEnum.enableNeighborhoodAmenities) return true + return false + } + + const { container } = render( + + + + + + ) + + await screen.findByRole("heading", { name: "Neighborhood amenities" }) + + // Check that Grid.Row elements are created (2 amenities = 1 row) + const rows = container.querySelectorAll('[class*="grid-row"]') + expect(rows.length).toBe(1) + }) +}) diff --git a/sites/partners/page_content/locale_overrides/general.json b/sites/partners/page_content/locale_overrides/general.json index 9ff341cf427..72bec6f8e5a 100644 --- a/sites/partners/page_content/locale_overrides/general.json +++ b/sites/partners/page_content/locale_overrides/general.json @@ -508,6 +508,16 @@ "nav.flags": "Flags", "nav.siteTitlePartners": "Partners Portal", "nav.users": "Users", + "neighborhoodAmenities.distance.onSite": "On site", + "neighborhoodAmenities.distance.oneBlock": "One block", + "neighborhoodAmenities.distance.twoBlocks": "Two blocks", + "neighborhoodAmenities.distance.threeBlocks": "Three blocks", + "neighborhoodAmenities.distance.fourBlocks": "Four blocks", + "neighborhoodAmenities.distance.fiveBlocks": "Five blocks", + "neighborhoodAmenities.distance.withinOneMile": "Within one mile", + "neighborhoodAmenities.distance.withinTwoMiles": "Within two miles", + "neighborhoodAmenities.distance.withinThreeMiles": "Within three miles", + "neighborhoodAmenities.distance.withinFourMiles": "Within four miles", "settings.createCopy": "Make a copy", "settings.createCopyDescription": "Create a copy of your preference.", "settings.preference": "Preference", diff --git a/sites/partners/src/components/listings/PaperListingForm/sections/NeighborhoodAmenities.tsx b/sites/partners/src/components/listings/PaperListingForm/sections/NeighborhoodAmenities.tsx index d9dde84fdeb..30900fb50c7 100644 --- a/sites/partners/src/components/listings/PaperListingForm/sections/NeighborhoodAmenities.tsx +++ b/sites/partners/src/components/listings/PaperListingForm/sections/NeighborhoodAmenities.tsx @@ -1,6 +1,6 @@ import React, { useContext, useMemo } from "react" import { useFormContext } from "react-hook-form" -import { t, Textarea } from "@bloom-housing/ui-components" +import { t, Textarea, Select } from "@bloom-housing/ui-components" import { Grid } from "@bloom-housing/ui-seeds" import SectionWithGrid from "../../../shared/SectionWithGrid" import { AuthContext } from "@bloom-housing/shared-helpers" @@ -10,6 +10,19 @@ import { } from "@bloom-housing/shared-helpers/src/types/backend-swagger" import { useJurisdiction } from "../../../../lib/hooks" +enum NeighborhoodAmenityDistanceEnum { + onSite = "onSite", + oneBlock = "oneBlock", + twoBlocks = "twoBlocks", + threeBlocks = "threeBlocks", + fourBlocks = "fourBlocks", + fiveBlocks = "fiveBlocks", + withinOneMile = "withinOneMile", + withinTwoMiles = "withinTwoMiles", + withinThreeMiles = "withinThreeMiles", + withinFourMiles = "withinFourMiles", +} + const NeighborhoodAmenities = () => { const formMethods = useFormContext() const { doJurisdictionsHaveFeatureFlagOn } = useContext(AuthContext) @@ -25,6 +38,21 @@ const NeighborhoodAmenities = () => { jurisdiction ) + const enableNeighborhoodAmenitiesDropdown = doJurisdictionsHaveFeatureFlagOn( + FeatureFlagEnum.enableNeighborhoodAmenitiesDropdown, + jurisdiction + ) + + const neighborhoodAmenityOptions = [ + "", + ...Object.values(NeighborhoodAmenityDistanceEnum).map((val) => { + return { + value: t(`neighborhoodAmenities.distance.${val}`), + label: t(`neighborhoodAmenities.distance.${val}`), + } + }), + ] + const visibleAmenities = useMemo(() => { const visibleAmenitiesList = jurisdictionData?.visibleNeighborhoodAmenities || [] return Object.values(NeighborhoodAmenitiesEnum).filter((amenity) => @@ -56,14 +84,25 @@ const NeighborhoodAmenities = () => { {row.map((amenity) => ( -