Skip to content

Commit

Permalink
add tests, tidy up
Browse files Browse the repository at this point in the history
  • Loading branch information
jessicamcinchak committed Jun 11, 2024
1 parent e28156f commit 0ade5c0
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 54 deletions.
46 changes: 16 additions & 30 deletions editor.planx.uk/src/@planx/components/List/Public/Context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
Schema,
UserData,
} from "../model";
import { flatten } from "../utils";
import {
flatten,
sumIdenticalUnits,
sumIdenticalUnitsByDevelopmentType,
} from "../utils";

interface ListContextValue {
schema: Schema;
Expand Down Expand Up @@ -132,7 +136,7 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
userData: getInitialValues(),
},
onSubmit: (values) => {
// defaultPassportData is used when coming "back"
// defaultPassportData (array) is used when coming "back"
const defaultPassportData = makeData(props, values.userData)?.["data"];

// flattenedPassportData makes individual list items compatible with Calculate components
Expand All @@ -141,40 +145,22 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
// basic example of general summary stats we can add onSubmit:
// 1. count of items/responses
// 2. if the schema includes a field that sets fn = "identicalUnits", sum of total units
// 3. if the schema includes a field that sets fn = "development", sum of total units by development "val"
let sumIdenticalUnits = 0;
defaultPassportData[`${props.fn}`].map(
(item) => (sumIdenticalUnits += parseInt(item?.identicalUnits)),
// 3. if the schema includes a field that sets fn = "development" & fn = "identicalUnits", sum of total units by development "val"
const totalUnits = sumIdenticalUnits(props.fn, defaultPassportData);
const totalUnitsByDevelopmentType = sumIdenticalUnitsByDevelopmentType(
props.fn,
defaultPassportData,
);

const sumIdenticalUnitsByDevelopmentType: Record<string, number> = {
newBuild: 0,
changeOfUseFrom: 0,
changeOfUseTo: 0,
};
defaultPassportData[`${props.fn}`].map(
(item) =>
(sumIdenticalUnitsByDevelopmentType[`${item?.development}`] +=
parseInt(item?.identicalUnits)),
);
const sumIdenticalUnitsByDevelopmentTypeSummary: Record<string, number> =
{};
Object.entries(sumIdenticalUnitsByDevelopmentType).forEach(([k, v]) => {
if (v > 0) {
sumIdenticalUnitsByDevelopmentTypeSummary[
`${props.fn}.total.units.development.${k}`
] = v;
}
});

const summaries = {
[`${props.fn}.total.listItems`]:
defaultPassportData[`${props.fn}`].length,
...(sumIdenticalUnits > 0 && {
[`${props.fn}.total.units`]: sumIdenticalUnits,
...(totalUnits > 0 && {
[`${props.fn}.total.units`]: totalUnits,
}),
...(Object.keys(sumIdenticalUnitsByDevelopmentTypeSummary).length > 0 &&
sumIdenticalUnitsByDevelopmentTypeSummary),
...(totalUnits > 0 &&
Object.keys(totalUnitsByDevelopmentType).length > 0 &&
totalUnitsByDevelopmentType),
};

handleSubmit?.({
Expand Down
68 changes: 66 additions & 2 deletions editor.planx.uk/src/@planx/components/List/Public/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import { cloneDeep, merge } from "lodash";
import React from "react";
import { axe, setup } from "testUtils";

import { UserResponse } from "../model";
import ListComponent, { Props } from "../Public";
import { GenericUnitsTest } from "../schemas/GenericUnitsTest";
import { Zoo } from "../schemas/Zoo";
import {
flatten,
sumIdenticalUnits,
sumIdenticalUnitsByDevelopmentType,
} from "../utils";

const mockProps: Props = {
fn: "mockFn",
Expand Down Expand Up @@ -57,7 +63,36 @@ const mockPropsUnits: Props = {

const mockPayloadUnits = {
data: {
"proposal.units.residential": [],
"proposal.units.residential": [
{
development: "newBuild",
garden: "Yes",
identicalUnits: 1,
},
{
development: "newBuild",
garden: "No",
identicalUnits: 2,
},
{
development: "changeOfUseTo",
garden: "No",
identicalUnits: 2,
},
],
"proposal.units.residential.one.development": "newBuild",
"proposal.units.residential.one.garden": "Yes",
"proposal.units.residential.one.identicalUnits": 1,
"proposal.units.residential.two.development": "newBuild",
"proposal.units.residential.two.garden": "No",
"proposal.units.residential.two.identicalUnits": 2,
"proposal.units.residential.three.development": "changeOfUseTo",
"proposal.units.residential.three.garden": "No",
"proposal.units.residential.three.identicalUnits": 2,
"proposal.units.residential.total.listItems": 3,
"proposal.units.residential.total.units": 5,
"proposal.units.residential.total.units.newBuid": 3,
"proposal.units.residential.total.units.changeOfUseTo": 2,
},
};

Expand Down Expand Up @@ -415,9 +450,38 @@ describe("Payload generation", () => {
const { getByTestId, user } = setup(
<ListComponent {...mockPropsUnits} handleSubmit={handleSubmit} />,
);

const saveButton = screen.getByRole("button", { name: /Save/ });
const addItemButton = getByTestId("list-add-button");
const developmentSelect = screen.getByRole("combobox");
const gardenYesRadio = screen.getAllByRole("radio")[0];
const gardenNoRadio = screen.getAllByRole("radio")[1];
const unitsNumberInput = screen.getByLabelText(/identical units/);

// Response 1
await user.click(developmentSelect);
await user.click(screen.getByRole("option", { name: /New build/ }));
await user.click(gardenYesRadio);
await user.type(unitsNumberInput, "1");
await user.click(saveButton);

// Response 2
await user.click(addItemButton);
await user.click(developmentSelect);
await user.click(screen.getByRole("option", { name: /New build/ }));
await user.click(gardenNoRadio);
await user.type(unitsNumberInput, "2");
await user.click(saveButton);

// fill in three unique responses
// Response 3
await user.click(addItemButton);
await user.click(developmentSelect);
await user.click(
screen.getByRole("option", { name: /Change of use to a home/ }),
);
await user.click(gardenNoRadio);
await user.type(unitsNumberInput, "2");
await user.click(saveButton);

await user.click(screen.getByTestId("continue-button"));

Expand Down
2 changes: 1 addition & 1 deletion editor.planx.uk/src/@planx/components/List/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export interface Schema {
max?: number;
}

type UserResponse = Record<Field["data"]["fn"], string>;
export type UserResponse = Record<Field["data"]["fn"], string>;

export type UserData = { userData: UserResponse[] };

Expand Down
77 changes: 77 additions & 0 deletions editor.planx.uk/src/@planx/components/List/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { UserResponse } from "./model";
import {
flatten,
sumIdenticalUnits,
sumIdenticalUnitsByDevelopmentType,
} from "./utils";

describe("passport data shape", () => {
it("flattens list items", async () => {
const defaultPassportData = {
mockFn: [
{
age: 10,
cuteness: "Very",
email: "[email protected]",
name: "Richard Parker",
size: "Medium",
},
{
age: 10,
cuteness: "Very",
email: "[email protected]",
name: "Richard Parker",
size: "Medium",
},
],
};

expect(flatten(defaultPassportData)).toEqual({
"mockFn.one.age": 10,
"mockFn.one.cuteness": "Very",
"mockFn.one.email": "[email protected]",
"mockFn.one.name": "Richard Parker",
"mockFn.one.size": "Medium",
"mockFn.two.age": 10,
"mockFn.two.cuteness": "Very",
"mockFn.two.email": "[email protected]",
"mockFn.two.name": "Richard Parker",
"mockFn.two.size": "Medium",
});
});

it("adds summary stats when applicable fields are set", async () => {
const defaultPassportData = {
"proposal.units.residential": [
{
development: "newBuild",
garden: "Yes",
identicalUnits: 1,
},
{
development: "newBuild",
garden: "No",
identicalUnits: 2,
},
{
development: "changeOfUseTo",
garden: "No",
identicalUnits: 2,
},
],
} as unknown as Record<string, UserResponse[]>;

expect(
sumIdenticalUnits("proposal.units.residential", defaultPassportData),
).toEqual(5);
expect(
sumIdenticalUnitsByDevelopmentType(
"proposal.units.residential",
defaultPassportData,
),
).toEqual({
"proposal.units.residential.total.units.development.newBuild": 3,
"proposal.units.residential.total.units.development.changeOfUseTo": 2,
});
});
});
98 changes: 77 additions & 21 deletions editor.planx.uk/src/@planx/components/List/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,77 @@
import { QuestionField, Schema } from "./model";
import { QuestionField, Schema, UserResponse } from "./model";

// Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}`
// Adapted from https://gist.github.com/penguinboy/762197
/**
* In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport
* @param value - the `val` or `text` of an Option defined in the schema's fields
* @param schema - the Schema object
* @returns string - the `text` for the given value `val`, or the original value
*/
export function formatSchemaDisplayValue(value: string, schema: Schema) {
const questionFields = schema.fields.filter(
(field) => field.type === "question",
) as QuestionField[];
const matchingField = questionFields?.find((field) =>
field.data.options.some((option) => option.data.val === value),
);
const matchingOption = matchingField?.data.options.find(
(option) => option.data.val === value,
);

// If we found a "val" match, return its text, else just return the value as passed in
return matchingOption?.data?.text || value;
}

/**
* If the schema includes a field that sets fn = "identicalUnits", sum of total units
* @param fn - passport key of current List
* @param passportData - default passport data format for the List
* @returns - sum of all units, or 0 if field not set
*/
export function sumIdenticalUnits(
fn: string,
passportData: Record<string, UserResponse[]>,
): number {
let sum = 0;
passportData[`${fn}`].map((item) => (sum += parseInt(item?.identicalUnits)));
return sum;
}

/**
* If the schema includes fields that set fn = "development" and fn = "identicalUnits", sum of total units by development option "val"
* @param fn - passport key of current List
* @param passportData - default passport data format for the List
* @returns - sum of all units by development type, or empty object if fields not set
*/
export function sumIdenticalUnitsByDevelopmentType(
fn: string,
passportData: Record<string, UserResponse[]>,
): Record<string, number> {
// Sum identical units by development type (read option `val` from Schema in future?)
const baseSums: Record<string, number> = {
newBuild: 0,
changeOfUseFrom: 0,
changeOfUseTo: 0,
};
passportData[`${fn}`].map(
(item) =>
(baseSums[`${item?.development}`] += parseInt(item?.identicalUnits)),
);

// Format property names for passport, and filter out any entries with default sum = 0
const formattedSums: Record<string, number> = {};
Object.entries(baseSums).forEach(([k, v]) => {
if (v > 0) {
formattedSums[`${fn}.total.units.development.${k}`] = v;
}
});

return formattedSums;
}

/**
* Flattens nested object so we can output passport variables like `{listFn}.{itemIndexAsText}.{fieldFn}`
* Adapted from https://gist.github.com/penguinboy/762197
*/
export function flatten<T extends Record<string, any>>(
object: T,
path: string | null = null,
Expand Down Expand Up @@ -30,8 +100,6 @@ export function flatten<T extends Record<string, any>>(
}, {} as T);
}

// Convert a whole number up to 99 to a spelled-out word (eg 34 => 'thirtyfour')
// Adapted from https://stackoverflow.com/questions/5529934/javascript-numbers-to-words
const ones = [
"",
"one",
Expand Down Expand Up @@ -80,26 +148,14 @@ function convertTens(num: number): string {
}
}

/**
* Convert a whole number up to 99 to a spelled-out word (eg 34 => 'thirtyfour')
* Adapted from https://stackoverflow.com/questions/5529934/javascript-numbers-to-words
*/
function convertNumberToText(num: number): string {
if (num == 0) {
return "zero";
} else {
return convertTens(num);
}
}

// In the case of "question" fields, ensure the displayed value reflects option "text", rather than "val" as recorded in passport
export function formatSchemaDisplayValue(value: string, schema: Schema) {
const questionFields = schema.fields.filter(
(field) => field.type === "question",
) as QuestionField[];
const matchingField = questionFields?.find((field) =>
field.data.options.some((option) => option.data.val === value),
);
const matchingOption = matchingField?.data.options.find(
(option) => option.data.val === value,
);

// If we found a "val" match, return its text, else just return the value as passed in
return matchingOption?.data?.text || value;
}

0 comments on commit 0ade5c0

Please sign in to comment.