From 260dd810df7f1e5ae03a91241e840a7223560201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:19:48 +0100 Subject: [PATCH 1/2] Hide compatibility parameters from reform search --- app/src/libs/metadataUtils.ts | 1 + .../tests/fixtures/libs/metadataUtilsMocks.ts | 46 ++ ...cyCreationModalParameterVisibilityMocks.ts | 649 ++++++++++++++++++ ...CreationModal.parameterVisibility.test.tsx | 180 +++++ app/src/tests/unit/libs/metadataUtils.test.ts | 53 ++ 5 files changed, 929 insertions(+) create mode 100644 app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts create mode 100644 app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx 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..bd65253bb --- /dev/null +++ b/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts @@ -0,0 +1,649 @@ +import type { MetadataApiPayload } from '@/types/metadata'; + +export const CANDIDATE_METADATA_PROVENANCE = { + model_commit: '1ea81b8af65eff995a3195770b57ce4fe61295e5', + API_commit: 'cd4039353dcf79171a6bf39241e4c47576d35242', + actual_metadata_sha256: '7f23cb58fc796bc24665ca34bed9ff283ac89aba06ebadfca36bd145b0d0fc7a', + prior13_HTTP_probe_sha256: '984f4af4093f1d7e1314f141606c5c0718cd8a595c144003868c9dabb27b0432', + qualification: + 'Exact parameter records copied from actual candidate-bound local API metadata. Unused variables/modules/policies/geographies/datasets removed; no fabricated search options. Prior payload is a literal copy of actual local API persisted request; not a new network assertion.', +} as const; + +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 PRIOR_ACTUAL_PERSISTENCE_PAYLOAD = { + 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate': { + '2026-01-01.2026-12-31': 0.1, + }, +}; +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..0a33124e0 --- /dev/null +++ b/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx @@ -0,0 +1,180 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +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, + PRIOR_ACTUAL_PERSISTENCE_PAYLOAD, + 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 evidence(name: string, value: unknown) { + const directory = process.env.POLICYENGINE_TEST_EVIDENCE_DIR; + if (directory) { + writeFileSync( + join(directory, name), + typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n` + ); + } +} +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); + evidence('final-API-boundary-calls.json', calls); + 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((context) => { + evidence( + `${context.task.name.replace(/[^a-z0-9]+/gi, '-').slice(0, 150)}.html`, + document.body.innerHTML + ); + cleanup(); + client.clear(); + vi.unstubAllGlobals(); +}); + +describe('genuine policy component 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)); + evidence( + canonical === CANONICAL_RATE ? 'main-search-options.json' : 'secondary-search-options.json', + options() + ); + 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)); + evidence('sentinel-search-options.json', options()); + for (const hidden of HIDDEN_SENTINELS) { + expect.soft(options()).not.toContain(hidden); + } + }, + COMPONENT_TEST_TIMEOUT + ); + + test( + 'given canonical selection when entering10percent for2026 and naming then the genuine API adapter submits canonical0.1', + 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); + evidence('canonical-setter10percent.html', document.body.innerHTML); + 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); + evidence('name-committed.html', document.body.innerHTML); + await user.click(screen.getByRole('button', { name: 'Create policy' })); + await waitFor(() => expect(created).toHaveBeenCalledTimes(1)); + expect(closed).toHaveBeenCalledTimes(1); + evidence('submission-payload.json', { + calls, + expected: EXPECTED_PAYLOAD, + priorActualPersistence: PRIOR_ACTUAL_PERSISTENCE_PAYLOAD, + created: created.mock.calls, + closedCount: closed.mock.calls.length, + qualification: + 'Only final fetch201 boundary mocked; actual selectors/providers/components/setter/hooks/adapter execute. Prior real HTTP persistence remains separate.', + }); + expect(calls).toHaveLength(1); + expect(calls[0].body?.data).toEqual(EXPECTED_PAYLOAD); + expect(calls[0].body?.data).toEqual(PRIOR_ACTUAL_PERSISTENCE_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' }, + ]); + }); +}); From 710e10748d1876820057796486a1c51df543359f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:02:25 +0100 Subject: [PATCH 2/2] Remove local evidence helpers from parameter visibility tests --- ...cyCreationModalParameterVisibilityMocks.ts | 14 ------- ...CreationModal.parameterVisibility.test.tsx | 40 ++----------------- 2 files changed, 3 insertions(+), 51 deletions(-) diff --git a/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts b/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts index bd65253bb..1ed369557 100644 --- a/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts +++ b/app/src/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks.ts @@ -1,14 +1,5 @@ import type { MetadataApiPayload } from '@/types/metadata'; -export const CANDIDATE_METADATA_PROVENANCE = { - model_commit: '1ea81b8af65eff995a3195770b57ce4fe61295e5', - API_commit: 'cd4039353dcf79171a6bf39241e4c47576d35242', - actual_metadata_sha256: '7f23cb58fc796bc24665ca34bed9ff283ac89aba06ebadfca36bd145b0d0fc7a', - prior13_HTTP_probe_sha256: '984f4af4093f1d7e1314f141606c5c0718cd8a595c144003868c9dabb27b0432', - qualification: - 'Exact parameter records copied from actual candidate-bound local API metadata. Unused variables/modules/policies/geographies/datasets removed; no fabricated search options. Prior payload is a literal copy of actual local API persisted request; not a new network assertion.', -} as const; - export const CANDIDATE_METADATA = { status: 'ok', message: null, @@ -641,9 +632,4 @@ 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 PRIOR_ACTUAL_PERSISTENCE_PAYLOAD = { - 'gov.hmrc.national_insurance.class_1.employee_scale[1].rate': { - '2026-01-01.2026-12-31': 0.1, - }, -}; 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 index 0a33124e0..e8a9fc395 100644 --- a/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx +++ b/app/src/tests/integration/pages/reportBuilder/PolicyCreationModal.parameterVisibility.test.tsx @@ -1,5 +1,3 @@ -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; 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'; @@ -15,7 +13,6 @@ import { HIDDEN_SENTINELS, MAIN_RATE_LABEL, POLICY_LABEL, - PRIOR_ACTUAL_PERSISTENCE_PAYLOAD, REPORT_YEAR, SEARCH_CASES, } from '@/tests/fixtures/pages/reportBuilder/PolicyCreationModalParameterVisibilityMocks'; @@ -23,15 +20,6 @@ import { const COMPONENT_TEST_TIMEOUT = 20_000; let client: QueryClient; let calls: Array<{ url: string; method: string; body: { data: unknown } | null }>; -function evidence(name: string, value: unknown) { - const directory = process.env.POLICYENGINE_TEST_EVIDENCE_DIR; - if (directory) { - writeFileSync( - join(directory, name), - typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n` - ); - } -} function mount() { const created = vi.fn(); const closed = vi.fn(); @@ -79,7 +67,6 @@ beforeEach(() => { body: init?.body ? JSON.parse(String(init.body)) : null, }; calls.push(request); - evidence('final-API-boundary-calls.json', calls); if (request.method !== 'POST' || request.url !== `${BASE_URL}/uk/policy`) { throw new Error(`Unexpected API boundary: ${JSON.stringify(request)}`); } @@ -91,17 +78,13 @@ beforeEach(() => { }) ); }); -afterEach((context) => { - evidence( - `${context.task.name.replace(/[^a-z0-9]+/gi, '-').slice(0, 150)}.html`, - document.body.innerHTML - ); +afterEach(() => { cleanup(); client.clear(); vi.unstubAllGlobals(); }); -describe('genuine policy component parameter visibility', () => { +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 }) => { @@ -109,10 +92,6 @@ describe('genuine policy component parameter visibility', () => { const user = userEvent.setup(); await user.type(screen.getByPlaceholderText('Search parameters...'), term); await waitFor(() => expect(screen.queryAllByRole('option').length).toBeGreaterThan(0)); - evidence( - canonical === CANONICAL_RATE ? 'main-search-options.json' : 'secondary-search-options.json', - options() - ); expect(options()).toEqual([canonical]); }, COMPONENT_TEST_TIMEOUT @@ -125,7 +104,6 @@ describe('genuine policy component parameter visibility', () => { const user = userEvent.setup(); await user.type(screen.getByPlaceholderText('Search parameters...'), 'threshold'); await waitFor(() => expect(screen.queryAllByRole('option').length).toBeGreaterThan(0)); - evidence('sentinel-search-options.json', options()); for (const hidden of HIDDEN_SENTINELS) { expect.soft(options()).not.toContain(hidden); } @@ -134,7 +112,7 @@ describe('genuine policy component parameter visibility', () => { ); test( - 'given canonical selection when entering10percent for2026 and naming then the genuine API adapter submits canonical0.1', + '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(); @@ -150,29 +128,17 @@ describe('genuine policy component parameter visibility', () => { await user.click(value); await user.keyboard('{Control>}a{/Control}10'); expect(value).toHaveValue(DISPLAY_PERCENT); - evidence('canonical-setter10percent.html', document.body.innerHTML); 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); - evidence('name-committed.html', document.body.innerHTML); await user.click(screen.getByRole('button', { name: 'Create policy' })); await waitFor(() => expect(created).toHaveBeenCalledTimes(1)); expect(closed).toHaveBeenCalledTimes(1); - evidence('submission-payload.json', { - calls, - expected: EXPECTED_PAYLOAD, - priorActualPersistence: PRIOR_ACTUAL_PERSISTENCE_PAYLOAD, - created: created.mock.calls, - closedCount: closed.mock.calls.length, - qualification: - 'Only final fetch201 boundary mocked; actual selectors/providers/components/setter/hooks/adapter execute. Prior real HTTP persistence remains separate.', - }); expect(calls).toHaveLength(1); expect(calls[0].body?.data).toEqual(EXPECTED_PAYLOAD); - expect(calls[0].body?.data).toEqual(PRIOR_ACTUAL_PERSISTENCE_PAYLOAD); expect(created.mock.calls[0][0].label).toBe(POLICY_LABEL); }, COMPONENT_TEST_TIMEOUT