diff --git a/app/src/libs/metadataUtils.ts b/app/src/libs/metadataUtils.ts index c1447df24..9c62a53fb 100644 --- a/app/src/libs/metadataUtils.ts +++ b/app/src/libs/metadataUtils.ts @@ -200,6 +200,7 @@ export const selectSearchableParameters = createSelector( .filter( (param): param is ParameterMetadata => param.type === 'parameter' && + !!(param.economy || param.household) && !!param.label && !EXCLUDED_PARAMETER_PATTERNS.some((pattern) => param.parameter.includes(pattern)) ) diff --git a/app/src/tests/fixtures/libs/metadataUtilsMocks.ts b/app/src/tests/fixtures/libs/metadataUtilsMocks.ts index 0cc2e7dbe..f81c7060b 100644 --- a/app/src/tests/fixtures/libs/metadataUtilsMocks.ts +++ b/app/src/tests/fixtures/libs/metadataUtilsMocks.ts @@ -1,6 +1,10 @@ import { transformMetadataPayload } from '@/libs/metadataUtils'; import type { RootState } from '@/store'; import type { MetadataApiPayload } from '@/types/metadata'; +import type { + ParameterMetadata, + ParameterMetadataCollection, +} from '@/types/metadata/parameterMetadata'; export const TEST_FIELD_NAMES = { STATE_NAME: 'state_name', @@ -103,3 +107,45 @@ export const mockStateWithMetadata = ( }, } as Partial; }; + +// Search visibility follows buildParameterTree; API defaults arrive as explicit true flags. +export const SEARCH_VISIBILITY_CASES: Array<{ + name: string; + flags: Pick; + visible: boolean; +}> = [ + { name: 'true true', flags: { economy: true, household: true }, visible: true }, + { name: 'true false', flags: { economy: true, household: false }, visible: true }, + { name: 'true absent', flags: { economy: true }, visible: true }, + { name: 'false true', flags: { economy: false, household: true }, visible: true }, + { name: 'false false', flags: { economy: false, household: false }, visible: false }, + { name: 'false absent', flags: { economy: false }, visible: false }, + { name: 'absent true', flags: { household: true }, visible: true }, + { name: 'absent false', flags: { household: false }, visible: false }, + { name: 'absent absent', flags: {}, visible: false }, +]; +export const VISIBLE_SEARCH_PARAMETER: ParameterMetadata = { + parameter: 'gov.income_tax.basic_rate', + label: 'Basic tax rate', + type: 'parameter', + economy: true, + household: true, +}; +export const SAME_LABEL_SEARCH_PARAMETERS: ParameterMetadataCollection = { + first: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.income_tax.basic_rate' }, + second: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.capital_gains.basic_rate' }, +}; +export const SORTED_SEARCH_PARAMETERS: ParameterMetadataCollection = { + zulu: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.tax.zulu', label: 'Zulu rate' }, + alpha: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.tax.alpha', label: 'Alpha rate' }, +}; +export const INELIGIBLE_SEARCH_PARAMETERS: ParameterMetadataCollection = { + node: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.income_tax', type: 'parameterNode' }, + unlabelled: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.tax.unlabelled', label: '' }, + cache: { ...VISIBLE_SEARCH_PARAMETER, parameter: 'gov.pycache.rate' }, +}; +export function searchVisibilityState( + parameters: ParameterMetadataCollection | undefined +): RootState { + return { metadata: { parameters } } as RootState; +} diff --git a/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts b/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts new file mode 100644 index 000000000..1ed369557 --- /dev/null +++ b/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts @@ -0,0 +1,635 @@ +import type { MetadataApiPayload } from '@/types/metadata'; + +export const CANDIDATE_METADATA = { + status: 'ok', + message: null, + result: { + variables: {}, + parameters: { + gov: { + type: 'parameterNode', + parameter: 'gov', + description: null, + label: 'Government', + economy: true, + household: true, + }, + 'gov.hmrc': { + type: 'parameterNode', + parameter: 'gov.hmrc', + description: null, + label: 'HMRC', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance', + description: null, + label: 'National Insurance', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1', + description: null, + label: 'Class 1', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale', + description: 'Class 1 employee marginal contribution schedule.', + label: 'Class 1 employee marginal contribution schedule', + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[0]': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[0]', + description: null, + label: 'bracket 1', + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[0].threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[0].threshold', + description: null, + label: 'threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '0001-01-01': 0, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[0].rate': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[0].rate', + description: null, + label: 'rate', + unit: '/1', + period: null, + values: { + '0001-01-01': 0, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[1]': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[1]', + description: null, + label: 'bracket 2', + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[1].threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[1].threshold', + description: + 'The Primary Threshold is the lower bound for the main rate of Class 1 National Insurance.', + label: 'NI primary threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 289.81, + '2038-01-01': 284.04, + '2037-01-01': 278.46, + '2036-01-01': 272.88, + '2035-01-01': 267.5, + '2034-01-01': 262.12, + '2033-01-01': 256.92, + '2032-01-01': 251.73, + '2031-01-01': 246.73, + '2030-04-06': 241.73, + '2029-04-06': 241.73, + '2028-04-06': 241.73, + '2027-04-06': 241.73, + '2026-04-06': 241.73, + '2022-07-01': 241.73, + '2022-04-06': 190, + '2021-04-06': 184, + '2020-04-06': 183, + '2019-04-06': 166, + '2018-04-06': 162, + '2017-04-06': 157, + '2016-04-06': 155, + '2015-04-06': 155, + '2015-01-01': 155, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate', + description: + 'The Class 1 National Insurance main rate is paid on employment earnings between the Primary Threshold and the Upper Earnings Limit.', + label: 'NI class 1 main rate', + unit: '/1', + period: null, + values: { + '2024-04-01': 0.08, + '2024-01-01': 0.1, + '2022-11-06': 0.12, + '2022-04-06': 0.1325, + '2015-04-01': 0.12, + '2015-01-01': 0.12, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[2]': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[2]', + description: null, + label: 'bracket 3', + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[2].threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[2].threshold', + description: + 'The Upper Earnings Limit is the upper bound for the main rate of Class 1 National Insurance.', + label: 'NI upper earnings limit', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 1164.81, + '2038-01-01': 1141.73, + '2037-01-01': 1118.85, + '2036-01-01': 1095.96, + '2035-01-01': 1073.27, + '2034-01-01': 1050.58, + '2033-01-01': 1028.08, + '2032-01-01': 1007.5, + '2031-01-01': 987.12, + '2030-04-06': 966.73, + '2029-04-06': 966.73, + '2028-04-06': 966.73, + '2027-04-06': 966.73, + '2026-04-06': 966.73, + '2021-04-06': 966.73, + '2020-04-06': 962, + '2019-04-06': 962, + '2018-04-06': 892, + '2017-04-06': 866, + '2016-04-06': 827, + '2015-04-06': 815, + '2015-01-01': 815, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employee_scale[2].rate': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employee_scale[2].rate', + description: + 'The Class 1 National Insurance additional rate is paid on employment earnings above the Upper Earnings Limit.', + label: 'NI class 1 additional rate', + unit: '/1', + period: null, + values: { + '2024-04-04': 0.02, + '2022-04-01': 0.0325, + '2015-04-01': 0.02, + '2015-01-01': 0.02, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employer_scale': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale', + description: 'Class 1 employer marginal contribution schedule.', + label: 'Class 1 employer marginal contribution schedule', + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[0]': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[0]', + description: null, + label: 'bracket 1', + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[0].threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[0].threshold', + description: null, + label: 'threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '0001-01-01': 0, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[0].rate': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[0].rate', + description: null, + label: 'rate', + unit: '/1', + period: null, + values: { + '0001-01-01': 0, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[1]': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[1]', + description: null, + label: 'bracket 2', + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[1].threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[1].threshold', + description: 'Secondary threshold for National Insurance.', + label: 'NI secondary threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 114.91274679905412, + '2038-01-01': 112.65955568534717, + '2037-01-01': 110.45054478955605, + '2036-01-01': 108.2848478328981, + '2035-01-01': 106.16161552244911, + '2034-01-01': 104.08001521808735, + '2033-01-01': 102.039230605968, + '2032-01-01': 100.0384613784, + '2031-01-01': 98.07692292, + '2030-04-06': 96.153846, + '2029-04-06': 96.153846, + '2028-04-06': 96.153846, + '2027-04-06': 96.153846, + '2026-04-06': 96.153846, + '2025-04-06': 96.153846, + '2024-01-01': 175, + '2022-04-06': 175, + '2021-04-06': 170, + '2020-04-06': 169, + '2019-04-06': 166, + '2018-04-06': 162, + '2017-04-06': 157, + '2016-04-06': 156, + '2015-04-06': 156, + '2015-01-01': 156, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.employer_scale[1].rate': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.employer_scale[1].rate', + description: + 'National Insurance contribution rate by employers on earnings above the Secondary Threshold.', + label: 'NI employer rate', + unit: '/1', + period: null, + values: { + '2025-01-01': 0.15, + '2022-11-06': 0.138, + '2022-04-06': 0.1505, + '2015-04-01': 0.138, + '2015-01-01': 0.138, + }, + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.rates': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.rates', + description: null, + label: 'Rates', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.rates.employee': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.rates.employee', + description: null, + label: 'Employee', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.rates.employee.main': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.rates.employee.main', + description: + 'The Class 1 National Insurance main rate is paid on employment earnings between the Primary Threshold and the Upper Earnings Limit.', + label: 'NI class 1 main rate', + unit: '/1', + period: null, + values: { + '2024-04-01': 0.08, + '2024-01-01': 0.1, + '2022-11-06': 0.12, + '2022-04-06': 0.1325, + '2015-04-01': 0.12, + '2015-01-01': 0.12, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.rates.employee.additional': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.rates.employee.additional', + description: + 'The Class 1 National Insurance additional rate is paid on employment earnings above the Upper Earnings Limit.', + label: 'NI class 1 additional rate', + unit: '/1', + period: null, + values: { + '2024-04-04': 0.02, + '2022-04-01': 0.0325, + '2015-04-01': 0.02, + '2015-01-01': 0.02, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.rates.employer': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.rates.employer', + description: + 'National Insurance contribution rate by employers on earnings above the Secondary Threshold.', + label: 'NI employer rate', + unit: '/1', + period: null, + values: { + '2025-01-01': 0.15, + '2022-11-06': 0.138, + '2022-04-06': 0.1505, + '2015-04-01': 0.138, + '2015-01-01': 0.138, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.thresholds': { + type: 'parameterNode', + parameter: 'gov.hmrc.national_insurance.class_1.thresholds', + description: null, + label: 'Thresholds', + economy: true, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.thresholds.lower_earnings_limit': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.thresholds.lower_earnings_limit', + description: 'Lower earnings limit for National Insurance.', + label: 'NI lower earnings limit', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 166.87407366945249, + '2038-01-01': 163.6021558238197, + '2037-01-01': 160.3944392855192, + '2036-01-01': 157.2493581690064, + '2035-01-01': 154.16612953150891, + '2034-01-01': 151.14318748748204, + '2033-01-01': 148.17974909415344, + '2032-01-01': 145.27424846597842, + '2031-01-01': 142.42590266018465, + '2030-01-01': 139.63314579122738, + '2029-01-01': 136.8951949163344, + '2028-01-01': 134.2112670927332, + '2027-01-01': 131.57979643487917, + '2026-04-06': 129, + '2025-04-06': 125, + '2022-04-06': 123, + '2021-04-06': 120, + '2020-04-06': 120, + '2019-04-06': 118, + '2018-04-06': 116, + '2017-04-06': 113, + '2016-04-06': 112, + '2015-04-06': 112, + '2015-01-01': 112, + }, + economy: false, + household: true, + }, + 'gov.hmrc.national_insurance.class_1.thresholds.primary_threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.thresholds.primary_threshold', + description: + 'The Primary Threshold is the lower bound for the main rate of Class 1 National Insurance.', + label: 'NI primary threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 289.81, + '2038-01-01': 284.04, + '2037-01-01': 278.46, + '2036-01-01': 272.88, + '2035-01-01': 267.5, + '2034-01-01': 262.12, + '2033-01-01': 256.92, + '2032-01-01': 251.73, + '2031-01-01': 246.73, + '2030-04-06': 241.73, + '2029-04-06': 241.73, + '2028-04-06': 241.73, + '2027-04-06': 241.73, + '2026-04-06': 241.73, + '2022-07-01': 241.73, + '2022-04-06': 190, + '2021-04-06': 184, + '2020-04-06': 183, + '2019-04-06': 166, + '2018-04-06': 162, + '2017-04-06': 157, + '2016-04-06': 155, + '2015-04-06': 155, + '2015-01-01': 155, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit', + description: + 'The Upper Earnings Limit is the upper bound for the main rate of Class 1 National Insurance.', + label: 'NI upper earnings limit', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 1164.81, + '2038-01-01': 1141.73, + '2037-01-01': 1118.85, + '2036-01-01': 1095.96, + '2035-01-01': 1073.27, + '2034-01-01': 1050.58, + '2033-01-01': 1028.08, + '2032-01-01': 1007.5, + '2031-01-01': 987.12, + '2030-04-06': 966.73, + '2029-04-06': 966.73, + '2028-04-06': 966.73, + '2027-04-06': 966.73, + '2026-04-06': 966.73, + '2021-04-06': 966.73, + '2020-04-06': 962, + '2019-04-06': 962, + '2018-04-06': 892, + '2017-04-06': 866, + '2016-04-06': 827, + '2015-04-06': 815, + '2015-01-01': 815, + }, + economy: false, + household: false, + }, + 'gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold': { + type: 'parameter', + parameter: 'gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold', + description: 'Secondary threshold for National Insurance.', + label: 'NI secondary threshold', + unit: 'currency-GBP', + period: 'week', + values: { + '2039-01-01': 114.91274679905412, + '2038-01-01': 112.65955568534717, + '2037-01-01': 110.45054478955605, + '2036-01-01': 108.2848478328981, + '2035-01-01': 106.16161552244911, + '2034-01-01': 104.08001521808735, + '2033-01-01': 102.039230605968, + '2032-01-01': 100.0384613784, + '2031-01-01': 98.07692292, + '2030-04-06': 96.153846, + '2029-04-06': 96.153846, + '2028-04-06': 96.153846, + '2027-04-06': 96.153846, + '2026-04-06': 96.153846, + '2025-04-06': 96.153846, + '2024-01-01': 175, + '2022-04-06': 175, + '2021-04-06': 170, + '2020-04-06': 169, + '2019-04-06': 166, + '2018-04-06': 162, + '2017-04-06': 157, + '2016-04-06': 156, + '2015-04-06': 156, + '2015-01-01': 156, + }, + economy: false, + household: false, + }, + }, + entities: { + person: { + plural: 'people', + label: 'Person', + doc: 'An individual. The minimal legal entity on which a legislation might be applied.', + is_person: true, + key: 'person', + roles: {}, + }, + benunit: { + plural: 'benunits', + label: 'Benefit unit', + doc: 'A family is also known as a benefit unit, and contains parents and dependent children.', + is_person: false, + key: 'benunit', + roles: { + member: { + plural: 'members', + label: 'Member', + doc: 'A person who is a citizen of a country.', + }, + }, + }, + household: { + plural: 'households', + label: 'Household', + doc: 'Each household may contain multiple families, or benefit units.', + is_person: false, + key: 'household', + roles: { + member: { + plural: 'members', + label: 'Member', + doc: 'A person who is a citizen of a country.', + }, + }, + }, + }, + variableModules: {}, + economy_options: { + region: [ + { + name: 'uk', + label: 'the UK', + type: 'national', + }, + ], + time_period: [ + { + name: 2024, + label: '2024', + }, + { + name: 2025, + label: '2025', + }, + { + name: 2026, + label: '2026', + }, + { + name: 2027, + label: '2027', + }, + { + name: 2028, + label: '2028', + }, + { + name: 2029, + label: '2029', + }, + { + name: 2030, + label: '2030', + }, + ], + datasets: [], + }, + current_law_id: 1, + basicInputs: [], + modelled_policies: { + core: {}, + filtered: {}, + }, + spm: { + available: false, + }, + version: '2.141.11', + }, +} as MetadataApiPayload; + +export const CANONICAL_RATE = 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate'; +export const REPORT_YEAR = '2026'; +export const DISPLAY_PERCENT = 10; +export const POLICY_LABEL = 'Local2490 component boundary'; +export const MAIN_RATE_LABEL = CANDIDATE_METADATA.result.parameters[CANONICAL_RATE].label; +export const SEARCH_CASES = [ + { + term: 'NI class 1 main rate', + canonical: 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate', + hidden: 'gov.hmrc.national_insurance.class_1.rates.employee.main', + }, + { + term: 'NI secondary threshold', + canonical: 'gov.hmrc.national_insurance.class_1.employer_scale[1].threshold', + hidden: 'gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold', + }, +]; +export const HIDDEN_SENTINELS = [ + 'gov.hmrc.national_insurance.class_1.employee_scale[0].threshold', + 'gov.hmrc.national_insurance.class_1.employer_scale[0].threshold', +]; +export const EXPECTED_PAYLOAD = { [CANONICAL_RATE]: { '2026-01-01.2026-12-31': 0.1 } }; diff --git a/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx b/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx new file mode 100644 index 000000000..e8a9fc395 --- /dev/null +++ b/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx @@ -0,0 +1,146 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { act, cleanup, renderWithCountry, screen, userEvent, waitFor } from '@test-utils'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { BASE_URL } from '@/constants'; +import { PolicyCreationModal } from '@/pages/reportBuilder/modals/PolicyCreationModal'; +import { fetchMetadataThunk } from '@/reducers/metadataReducer'; +import { store } from '@/store'; +import { + CANDIDATE_METADATA, + CANONICAL_RATE, + DISPLAY_PERCENT, + EXPECTED_PAYLOAD, + HIDDEN_SENTINELS, + MAIN_RATE_LABEL, + POLICY_LABEL, + REPORT_YEAR, + SEARCH_CASES, +} from '@/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks'; + +const COMPONENT_TEST_TIMEOUT = 20_000; +let client: QueryClient; +let calls: Array<{ url: string; method: string; body: { data: unknown } | null }>; +function mount() { + const created = vi.fn(); + const closed = vi.fn(); + renderWithCountry( + + + , + 'uk', + '/uk/reports/create', + 'calculator' + ); + return { created, closed }; +} +function options() { + return screen.queryAllByRole('option').map((option) => option.getAttribute('data-value')); +} +beforeEach(() => { + calls = []; + client = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, + }); + localStorage.clear(); + act(() => { + store.dispatch(fetchMetadataThunk.pending('candidate2490-metadata', 'uk')); + store.dispatch( + fetchMetadataThunk.fulfilled( + { data: structuredClone(CANDIDATE_METADATA), country: 'uk' }, + 'candidate2490-metadata', + 'uk' + ) + ); + }); + vi.stubGlobal( + 'fetch', + vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => { + const request = { + url: String(url), + method: init?.method || 'GET', + body: init?.body ? JSON.parse(String(init.body)) : null, + }; + calls.push(request); + if (request.method !== 'POST' || request.url !== `${BASE_URL}/uk/policy`) { + throw new Error(`Unexpected API boundary: ${JSON.stringify(request)}`); + } + return { + ok: true, + status: 201, + json: async () => ({ status: 'ok', result: { policy_id: 'component-boundary-only-2' } }), + } as Response; + }) + ); +}); +afterEach(() => { + cleanup(); + client.clear(); + vi.unstubAllGlobals(); +}); + +describe('PolicyCreationModal parameter visibility', () => { + test.each(SEARCH_CASES)( + 'given $term with a hidden compatibility alias when searching then only its canonical option appears', + async ({ term, canonical }) => { + mount(); + const user = userEvent.setup(); + await user.type(screen.getByPlaceholderText('Search parameters...'), term); + await waitFor(() => expect(screen.queryAllByRole('option').length).toBeGreaterThan(0)); + expect(options()).toEqual([canonical]); + }, + COMPONENT_TEST_TIMEOUT + ); + + test( + 'given hidden fixed-zero sentinels when searching their label then they are absent', + async () => { + mount(); + const user = userEvent.setup(); + await user.type(screen.getByPlaceholderText('Search parameters...'), 'threshold'); + await waitFor(() => expect(screen.queryAllByRole('option').length).toBeGreaterThan(0)); + for (const hidden of HIDDEN_SENTINELS) { + expect.soft(options()).not.toContain(hidden); + } + }, + COMPONENT_TEST_TIMEOUT + ); + + test( + 'given canonical selection when entering a 10 percent value for 2026 and naming the policy then the API adapter submits the canonical 0.1 rate', + async () => { + const { created, closed } = mount(); + const user = userEvent.setup(); + await user.type(screen.getByPlaceholderText('Search parameters...'), MAIN_RATE_LABEL); + await waitFor(() => expect(screen.queryAllByRole('option').length).toBeGreaterThan(0)); + const canonical = screen + .getAllByRole('option') + .find((option) => option.getAttribute('data-value') === CANONICAL_RATE); + expect(canonical).toBeDefined(); + await user.click(canonical!); + await user.click(screen.getByRole('tab', { name: 'Yearly' })); + const value = screen.getByPlaceholderText('Enter value'); + await user.click(value); + await user.keyboard('{Control>}a{/Control}10'); + expect(value).toHaveValue(DISPLAY_PERCENT); + await user.click(screen.getByRole('button', { name: 'Add change' })); + await user.click(screen.getByText('Click to name your policy...', { exact: true })); + await user.type(screen.getByPlaceholderText('Enter policy name...'), POLICY_LABEL); + await user.keyboard('{Enter}'); + expect(screen.queryByPlaceholderText('Enter policy name...')).not.toBeInTheDocument(); + expect(screen.getAllByText(POLICY_LABEL, { exact: true }).length).toBeGreaterThan(0); + await user.click(screen.getByRole('button', { name: 'Create policy' })); + await waitFor(() => expect(created).toHaveBeenCalledTimes(1)); + expect(closed).toHaveBeenCalledTimes(1); + expect(calls).toHaveLength(1); + expect(calls[0].body?.data).toEqual(EXPECTED_PAYLOAD); + expect(created.mock.calls[0][0].label).toBe(POLICY_LABEL); + }, + COMPONENT_TEST_TIMEOUT + ); +}); diff --git a/app/src/tests/unit/libs/metadataUtils.test.ts b/app/src/tests/unit/libs/metadataUtils.test.ts index bacccf0f2..58506e46a 100644 --- a/app/src/tests/unit/libs/metadataUtils.test.ts +++ b/app/src/tests/unit/libs/metadataUtils.test.ts @@ -3,15 +3,22 @@ import { getFieldLabel, getFieldOptions, isDropdownField, + selectSearchableParameters, transformMetadataPayload, } from '@/libs/metadataUtils'; import type { RootState } from '@/store'; import { EXPECTED_LABELS, + INELIGIBLE_SEARCH_PARAMETERS, mockMetadataPayload, mockMinimalPayload, mockStateWithMetadata, + SAME_LABEL_SEARCH_PARAMETERS, + SEARCH_VISIBILITY_CASES, + searchVisibilityState, + SORTED_SEARCH_PARAMETERS, TEST_FIELD_NAMES, + VISIBLE_SEARCH_PARAMETER, } from '@/tests/fixtures/libs/metadataUtilsMocks'; describe('metadataUtils', () => { @@ -266,3 +273,49 @@ describe('metadataUtils', () => { }); }); }); + +describe('selectSearchableParameters visibility', () => { + it.each(SEARCH_VISIBILITY_CASES)( + 'given $name visibility flags then search follows tree visibility', + ({ flags, visible }) => { + const parameter = { ...VISIBLE_SEARCH_PARAMETER }; + delete parameter.economy; + delete parameter.household; + Object.assign(parameter, flags); + const state = searchVisibilityState({ [parameter.parameter]: parameter }); + expect(selectSearchableParameters(state)).toEqual( + visible ? [{ value: parameter.parameter, label: parameter.label }] : [] + ); + } + ); + it('given API default true visibility then the parameter remains searchable', () => { + expect( + selectSearchableParameters(searchVisibilityState({ rate: VISIBLE_SEARCH_PARAMETER })) + ).toEqual([ + { value: VISIBLE_SEARCH_PARAMETER.parameter, label: VISIBLE_SEARCH_PARAMETER.label }, + ]); + }); + it('given no parameters then search remains empty', () => { + expect(selectSearchableParameters(searchVisibilityState({}))).toEqual([]); + expect(selectSearchableParameters(searchVisibilityState(undefined))).toEqual([]); + }); + it('given visible nodes or unlabelled or excluded paths then original filters still exclude them', () => { + expect(selectSearchableParameters(searchVisibilityState(INELIGIBLE_SEARCH_PARAMETERS))).toEqual( + [] + ); + }); + it('given distinct visible IDs with equal labels then both remain searchable', () => { + expect(selectSearchableParameters(searchVisibilityState(SAME_LABEL_SEARCH_PARAMETERS))).toEqual( + [ + { value: 'gov.income_tax.basic_rate', label: 'Basic tax rate' }, + { value: 'gov.capital_gains.basic_rate', label: 'Basic tax rate' }, + ] + ); + }); + it('given labels in reverse order then original alphabetical sort remains', () => { + expect(selectSearchableParameters(searchVisibilityState(SORTED_SEARCH_PARAMETERS))).toEqual([ + { value: 'gov.tax.alpha', label: 'Alpha rate' }, + { value: 'gov.tax.zulu', label: 'Zulu rate' }, + ]); + }); +});