diff --git a/cypress/e2e/state-calculator.cy.ts b/cypress/e2e/state-calculator.cy.ts index 0b731dd..715d4fd 100644 --- a/cypress/e2e/state-calculator.cy.ts +++ b/cypress/e2e/state-calculator.cy.ts @@ -88,6 +88,12 @@ describe('rewiring-america-state-calculator', () => { .checkA11y(null, { runOnly: ['wcag2a', 'wcag2aa'], }); + + cy.selectProjects(['cooking']); + + cy.get('rewiring-america-state-calculator') + .shadow() + .contains('Up to $420 off an electric/induction stove'); }); it('shows an error if you query in the wrong state', () => { diff --git a/src/item-name.ts b/src/item-name.ts index 9d344eb..428f0ac 100644 --- a/src/item-name.ts +++ b/src/item-name.ts @@ -1,5 +1,6 @@ import { ItemType } from './api/calculator-types-v1'; import { MsgFn } from './i18n/use-translated'; +import { Project } from './projects'; type ItemGroup = | 'air_source_heat_pump' @@ -14,7 +15,8 @@ type ItemGroup = | 'weatherization' | 'audit_and_weatherization' | 'water_heater' - | 'electric_outdoor_equipment'; + | 'electric_outdoor_equipment' + | 'hear_projects'; const ALL_INSULATION: ItemType[] = [ 'attic_or_roof_insulation', @@ -114,13 +116,17 @@ const ITEM_GROUPS: { group: ItemGroup; members: Set }[] = [ group: 'water_heater', members: new Set(['heat_pump_water_heater', 'non_heat_pump_water_heater']), }, + { + group: 'hear_projects', + members: new Set(['heat_pump_clothes_dryer', 'electric_stove']), + }, ]; const itemsBelongToGroup = (items: ItemType[], members: Set) => { return items.every(i => members.has(i)); }; -const multipleItemsName = (items: ItemType[], msg: MsgFn) => { +const multipleItemsName = (items: ItemType[], msg: MsgFn, project: Project) => { // For a multiple-items case, check whether all the items are in one of the // defined groups. for (const { group, members } of ITEM_GROUPS) { @@ -174,6 +180,8 @@ const multipleItemsName = (items: ItemType[], msg: MsgFn) => { return msg('electric outdoor equipment', { desc: 'e.g. "$100 off [this string]"', }); + case 'hear_projects': + return hearName(items, msg, project); default: { // This will be a type error if the above switch is not exhaustive const unknownGroup: never = group; @@ -186,12 +194,44 @@ const multipleItemsName = (items: ItemType[], msg: MsgFn) => { return null; }; +const hearName = (items: ItemType[], msg: MsgFn, project: Project) => { + const HEAR_INCENTIVE_PROJECT_MSG_LIST: { + item: ItemType; + project: Project; + msg: string; + }[] = [ + { + item: 'heat_pump_clothes_dryer', + project: 'clothes_dryer', + msg: msg('a heat pump clothes dryer', { + desc: 'e.g. "$100 off [this string]"', + }), + }, + + { + item: 'electric_stove', + project: 'cooking', + msg: msg('an electric/induction stove', { + desc: 'e.g. "$100 off [this string]"', + }), + }, + ]; + + const match = HEAR_INCENTIVE_PROJECT_MSG_LIST.find( + group => items.includes(group.item) && project === group.project, + ); + + if (!match) return null; + + return match.msg; +}; + /** * TODO this is an internationalization sin. Figure out something better! */ -export const itemName = (items: ItemType[], msg: MsgFn) => { +export const itemName = (items: ItemType[], msg: MsgFn, project: Project) => { if (items.length > 1) { - return multipleItemsName(items, msg); + return multipleItemsName(items, msg, project); } if (items.length !== 1) { diff --git a/src/state-incentive-details.tsx b/src/state-incentive-details.tsx index fed4fe7..72811f7 100644 --- a/src/state-incentive-details.tsx +++ b/src/state-incentive-details.tsx @@ -33,8 +33,8 @@ const formatUnit = (unit: AmountUnit, msg: MsgFn) => ? msg('watt') : unit; -const formatTitle = (incentive: Incentive, msg: MsgFn) => { - const item = itemName(incentive.items, msg); +const formatTitle = (incentive: Incentive, msg: MsgFn, project: Project) => { + const item = itemName(incentive.items, msg, project); if (!item) { return null; } @@ -145,6 +145,7 @@ const renderCardCollection = ( incentives: Incentive[], iraRebates: IRARebate[], showComingSoon: boolean, + project: Project, ) => { const { msg } = useTranslated(); return ( @@ -156,7 +157,7 @@ const renderCardCollection = ( (getStartYearIfInFuture(a) ?? 0) - (getStartYearIfInFuture(b) ?? 0), ) .map((incentive, index) => { - const headline = formatTitle(incentive, msg); + const headline = formatTitle(incentive, msg, project); if (!headline) { // We couldn't generate a headline either because the items are // unknown, or the amount type is unknown. Don't show a card. @@ -271,7 +272,12 @@ const IncentiveGrid = forwardRef( /> {selectedTab !== null - ? renderCardCollection(incentives, iraRebates, !hasStateCoverage) + ? renderCardCollection( + incentives, + iraRebates, + !hasStateCoverage, + selectedTab, + ) : renderSelectProjectCard()} ); diff --git a/test/item-names.test.ts b/test/item-names.test.ts index 81d9d3c..ddefbfb 100644 --- a/test/item-names.test.ts +++ b/test/item-names.test.ts @@ -4,47 +4,80 @@ import { itemName } from '../src/item-name'; describe('group names', () => { test('heat pumps', () => { - expect(itemName(['ducted_heat_pump', 'ductless_heat_pump'], msg)).toBe( - 'an air source heat pump', - ); expect( - itemName(['ducted_heat_pump', 'geothermal_heating_installation'], msg), + itemName(['ducted_heat_pump', 'ductless_heat_pump'], msg, 'hvac'), + ).toBe('an air source heat pump'); + expect( + itemName( + ['ducted_heat_pump', 'geothermal_heating_installation'], + msg, + 'hvac', + ), ).toBe('a heat pump'); }); test('weatherization and insulation', () => { expect( - itemName(['attic_or_roof_insulation', 'basement_insulation'], msg), + itemName( + ['attic_or_roof_insulation', 'basement_insulation'], + msg, + 'weatherization_and_efficiency', + ), ).toBe('insulation'); - expect(itemName(['attic_or_roof_insulation', 'air_sealing'], msg)).toBe( - 'weatherization', - ); - expect(itemName(['wall_insulation', 'other_insulation'], msg)).toBe( - 'insulation', - ); - expect(itemName(['wall_insulation', 'other_weatherization'], msg)).toBe( - 'weatherization', - ); - expect(itemName(['other_weatherization', 'energy_audit'], msg)).toBe( - 'an energy audit and weatherization', - ); + expect( + itemName( + ['attic_or_roof_insulation', 'air_sealing'], + msg, + 'weatherization_and_efficiency', + ), + ).toBe('weatherization'); + expect( + itemName( + ['wall_insulation', 'other_insulation'], + msg, + 'weatherization_and_efficiency', + ), + ).toBe('insulation'); + expect( + itemName( + ['wall_insulation', 'other_weatherization'], + msg, + 'weatherization_and_efficiency', + ), + ).toBe('weatherization'); + expect( + itemName( + ['other_weatherization', 'energy_audit'], + msg, + 'weatherization_and_efficiency', + ), + ).toBe('an energy audit and weatherization'); }); test('vehicles', () => { expect( - itemName(['new_electric_vehicle', 'used_electric_vehicle'], msg), + itemName(['new_electric_vehicle', 'used_electric_vehicle'], msg, 'ev'), ).toBe('an electric vehicle'); expect( itemName( ['new_plugin_hybrid_vehicle', 'used_plugin_hybrid_vehicle'], msg, + 'ev', ), ).toBe('a plug-in hybrid'); expect( - itemName(['new_electric_vehicle', 'new_plugin_hybrid_vehicle'], msg), + itemName( + ['new_electric_vehicle', 'new_plugin_hybrid_vehicle'], + msg, + 'ev', + ), ).toBe('a new vehicle'); expect( - itemName(['used_electric_vehicle', 'used_plugin_hybrid_vehicle'], msg), + itemName( + ['used_electric_vehicle', 'used_plugin_hybrid_vehicle'], + msg, + 'ev', + ), ).toBe('a used vehicle'); expect( @@ -56,7 +89,22 @@ describe('group names', () => { 'used_plugin_hybrid_vehicle', ], msg, + 'ev', ), ).toBeNull(); }); + + test('HEAR rebates applicable to multiple appliances', () => { + expect( + itemName(['heat_pump_clothes_dryer', 'electric_stove'], msg, 'cooking'), + ).toBe('an electric/induction stove'); + + expect( + itemName( + ['heat_pump_clothes_dryer', 'electric_stove'], + msg, + 'clothes_dryer', + ), + ).toBe('a heat pump clothes dryer'); + }); });