complete more of the unit tests.

This commit is contained in:
Jordan Hewitt 2024-07-01 08:05:24 -07:00
parent 76fe4eb34a
commit 379f43dcd9
8 changed files with 137 additions and 76 deletions

View File

@ -1,27 +1,30 @@
import { View } from "react-native-reanimated/lib/typescript/Animated";
import { MeasurementInput } from "./MeasurementInput"; import { MeasurementInput } from "./MeasurementInput";
import { area_t, dimensions_t } from "@/lib/product"; import { area_t, dimensions_t } from "@/lib/product";
import { useState } from "react"; import { useState } from "react";
import { View } from "react-native";
export type AreaInputProps = { export type AreaInputProps = {
units: "foot" | "inch",
onMeasurementSet?: (area : dimensions_t) => any, onMeasurementSet?: (area : dimensions_t) => any,
defaultValue?: area_t,
lengthLabel?: string,
widthLabel?: string,
} }
export function AreaInput({units, onMeasurementSet} : AreaInputProps) { export function AreaInput({onMeasurementSet, lengthLabel, widthLabel, defaultValue} : AreaInputProps) {
const [area, setArea] = useState({ defaultValue = defaultValue || {l: 0, w: 0, u: "foot"}
l: 0,
w: 0, const [area, setArea] = useState(defaultValue)
u: "foot",
} as area_t)
function doOnLengthSet(measurement : dimensions_t) { function doOnLengthSet(measurement : dimensions_t) {
setArea({ setArea({
...area, ...area,
l: measurement.l l: measurement.l
}); });
onMeasurementSet && onMeasurementSet(area); onMeasurementSet && onMeasurementSet({
...area,
l: measurement.l
});
} }
function doOnWidthSet(measurement : dimensions_t) { function doOnWidthSet(measurement : dimensions_t) {
@ -29,20 +32,25 @@ export function AreaInput({units, onMeasurementSet} : AreaInputProps) {
...area, ...area,
w: measurement.l w: measurement.l
}); });
onMeasurementSet && onMeasurementSet(area); onMeasurementSet && onMeasurementSet({
...area,
w: measurement.l
});
} }
return ( return (
<View> <View>
<MeasurementInput <MeasurementInput
units={units} units={area.u}
defaultValue={area.l} defaultValue={area.l}
onValueSet={doOnLengthSet} onValueSet={doOnLengthSet}
label={lengthLabel}
/> />
<MeasurementInput <MeasurementInput
units={units} units={area.u}
defaultValue={area.w} defaultValue={area.w}
onValueSet={doOnWidthSet} onValueSet={doOnWidthSet}
label={widthLabel}
/> />
</View> </View>
) )

View File

@ -1,32 +1,38 @@
import { dimensions_t, length_t } from "@/lib/product"; import { dimensions_t, length_t } from "@/lib/product";
import { Length } from "convert";
import { StyleSheet, Text, TextInput, View } from "react-native"; import { StyleSheet, Text, TextInput, View } from "react-native";
export type t_length_unit = "foot" | "inch" export type t_length_unit = "foot" | "inch"
export type MeasurementInputProps = { export type MeasurementInputProps = {
onValueSet?: (d: dimensions_t) => any, onValueSet?: (d: dimensions_t) => any,
units: t_length_unit, defaultValue: length_t;
defaultValue: number; label?: string,
} }
export function MeasurementInput({onValueSet, units, defaultValue}: MeasurementInputProps) { export function MeasurementInput({onValueSet, defaultValue, label}: MeasurementInputProps) {
1
function doOnValueSet(value : string) { function doOnValueSet(value : string) {
const iVal = parseFloat(value) || parseInt(value);
onValueSet && onValueSet({ onValueSet && onValueSet({
l: (parseInt(value) || parseFloat(value)), ...defaultValue,
u: units, l: iVal,
}) })
} }
const sDefValue = new String(defaultValue.l).valueOf()
return ( return (
<View> <View>
<TextInput <TextInput
clearTextOnFocus={true} clearTextOnFocus={true}
defaultValue={new String(defaultValue).valueOf()} defaultValue={sDefValue}
onChangeText={doOnValueSet} onChangeText={doOnValueSet}
inputMode='decimal' inputMode='decimal'
style={styles.lengthInput} /> style={styles.lengthInput}
<Text style={styles.unitHints}>{units}</Text> aria-label={label || "Enter measurement"}
/>
<Text style={styles.unitHints}>{defaultValue.u}</Text>
</View> </View>
) )
} }

View File

@ -1,5 +1,4 @@
import { StyleSheet, Text } from "react-native"; import { StyleSheet, Text, View } from "react-native";
import { View } from "react-native-reanimated/lib/typescript/Animated";
export type PriceDisplayProps = { export type PriceDisplayProps = {
price: number, price: number,
@ -13,7 +12,7 @@ export default function PriceDisplay({ price }: PriceDisplayProps) {
return ( return (
<View style={styles.bigPriceWrapper}> <View style={styles.bigPriceWrapper} aria-label="calculated price">
<Text style={styles.bigPrice}>$ {price.toLocaleString( <Text style={styles.bigPrice}>$ {price.toLocaleString(
undefined, { undefined, {
minimumFractionDigits: 2, minimumFractionDigits: 2,

View File

@ -8,6 +8,9 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import PriceDisplay from './Price'; import PriceDisplay from './Price';
import { AreaInput } from './AreaInput'; import { AreaInput } from './AreaInput';
import { MeasurementInput } from './MeasurementInput'; import { MeasurementInput } from './MeasurementInput';
import ProductList from './ProductList';
import UnitChooser from './UnitChooser';
import { Length } from 'convert';
export default function ProductCalculatorSelector() { export default function ProductCalculatorSelector() {
@ -15,7 +18,7 @@ export default function ProductCalculatorSelector() {
const products = useAppSelector(selectProducts); const products = useAppSelector(selectProducts);
const [activeProduct, setActiveProduct] = useState(null as Product | null); const [activeProduct, setActiveProduct] = useState(null as Product | null);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [measurement, setMeasurement] = useState(null as dimensions_t | null); const [measurement, setMeasurement] = useState({l: 0, w: 0, u: "ft"} as dimensions_t);
useEffect(function () { useEffect(function () {
const iv = setInterval(function () { const iv = setInterval(function () {
@ -33,6 +36,13 @@ export default function ProductCalculatorSelector() {
setMeasurement(dimensions); setMeasurement(dimensions);
} }
function onUnitChosen(unit : Length) {
setMeasurement({
...measurement,
u: unit,
});
}
return ( return (
<SafeAreaView style={styles.wrapper}> <SafeAreaView style={styles.wrapper}>
<PriceDisplay price={price} /> <PriceDisplay price={price} />
@ -42,23 +52,28 @@ export default function ProductCalculatorSelector() {
activeProduct ? ( activeProduct ? (
"w" in activeProduct.dimensions ? "w" in activeProduct.dimensions ?
<AreaInput <AreaInput
units={units} defaultValue={activeProduct.dimensions}
onMeasurementSet={onMeasurementSet} onMeasurementSet={onMeasurementSet}
widthLabel='enter width'
lengthLabel='enter length'
/> />
: :
<MeasurementInput <MeasurementInput
defaultValue={activeProduct.dimensions.l} defaultValue={activeProduct.dimensions}
units={units}
onValueSet={onMeasurementSet} onValueSet={onMeasurementSet}
label="enter length"
/> />
) : ( ) : (
<Text>Please select a product</Text> <Text>Please select a product</Text>
) )
} }
{
activeProduct && <UnitChooser choices={["in", "ft"]} onChoicePressed={onUnitChosen} />
}
</View> </View>
</View> </View>
<ProductList onProductSelected={setActiveProduct} />
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@ -33,10 +33,7 @@ export function ProductTile ({product, onProductSelected, isActive, style} : Pro
<TouchableHighlight <TouchableHighlight
style={_style.highlight || styles.highlight} style={_style.highlight || styles.highlight}
onPress={() => onProductSelected && onProductSelected(product)}> onPress={() => onProductSelected && onProductSelected(product)}>
<Text style={_style.text || styles.text}> <Text style={_style.text || styles.text}>{product.attributes.name || `Product ${product.id}`} ({product.pricePerUnitDisplay})</Text>
{product.attributes.name || `Product ${product.id}`}
({product.pricePerUnitDisplay})
</Text>
</TouchableHighlight> </TouchableHighlight>
); );
} }

View File

@ -1,23 +1,31 @@
import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { AreaInput } from '../AreaInput'; import { AreaInput } from '../AreaInput';
describe('AreaInput', () => { describe('AreaInput', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { getByPlaceholderText } = render(<AreaInput units="foot" />); render(<AreaInput lengthLabel='length' widthLabel='width' />);
const lengthInput = getByPlaceholderText('Length'); const lengthInput = screen.getByLabelText('length');
const widthInput = getByPlaceholderText('Width'); const widthInput = screen.getByLabelText('width');
expect(lengthInput).toBeTruthy(); expect(lengthInput).toBeTruthy();
expect(widthInput).toBeTruthy(); expect(widthInput).toBeTruthy();
}); });
it('calls onValueSet when a value is entered', () => { it('calls onValueSet when a value is entered', () => {
const onValueSetMock = jest.fn(); const onMeasurementSetMock = jest.fn();
const { getByPlaceholderText } = render( render(<AreaInput onMeasurementSet={onMeasurementSetMock} lengthLabel='length' widthLabel='width' defaultValue={{l: 4, w:4, u: "inch"}}/>);
<AreaInput units="foot" onValueSet={onValueSetMock} /> const lengthInput = screen.getByLabelText('length');
); const widthInput = screen.getByLabelText('width');
const lengthInput = getByPlaceholderText('Length');
fireEvent.changeText(lengthInput, '10'); fireEvent.changeText(lengthInput, '10');
expect(onValueSetMock).toHaveBeenCalledTimes(1); expect(onMeasurementSetMock).toHaveBeenCalledWith({
l: 10,
w: 4,
u: "inch"
});
fireEvent.changeText(widthInput, '10');
expect(onMeasurementSetMock).toHaveBeenCalledWith({
l: 10,
w: 10,
u: "inch"
});
}); });
}); });

View File

@ -1,17 +1,17 @@
import { render, fireEvent } from '@testing-library/react-native'; import { render, fireEvent, screen } from '@testing-library/react-native';
import { MeasurementInput } from '../MeasurementInput'; import { MeasurementInput } from '../MeasurementInput';
describe('MeasurementInput', () => { describe('MeasurementInput', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { getByPlaceholderText } = render(<MeasurementInput units="foot" defaultValue={10} />); render(<MeasurementInput units="foot" defaultValue={10} />);
const input = getByPlaceholderText('Enter measurement'); const input = screen.getByLabelText('Enter measurement');
expect(input).toBeTruthy(); expect(input).toBeTruthy();
}); });
it('calls onValueSet when value is changed', () => { it('calls onValueSet when value is changed', () => {
const mockOnValueSet = jest.fn(); const mockOnValueSet = jest.fn();
const { getByPlaceholderText } = render(<MeasurementInput units="foot" defaultValue={10} onValueSet={mockOnValueSet} />); render(<MeasurementInput units="foot" defaultValue={10} onValueSet={mockOnValueSet} />);
const input = getByPlaceholderText('Enter measurement'); const input = screen.getByLabelText('Enter measurement');
fireEvent.changeText(input, '20'); fireEvent.changeText(input, '20');
expect(mockOnValueSet).toHaveBeenCalledWith({ l: 20, u: 'foot' }); expect(mockOnValueSet).toHaveBeenCalledWith({ l: 20, u: 'foot' });
}); });

View File

@ -1,42 +1,70 @@
import React from 'react'; import { render, fireEvent, screen, act } from '@testing-library/react-native';
import { render, fireEvent } from '@testing-library/react-native';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { store } from '@/app/store';
import ProductCalculatorSelector from '@/components/ProductCalculatorSelector'; import ProductCalculatorSelector from '@/components/ProductCalculatorSelector';
import { renderWithProviders } from '@/lib/rendering';
import { Product } from '@/lib/product';
describe('ProductCalculatorSelector', () => { describe('ProductCalculatorSelector', () => {
it('renders correctly', () => {
const { getByText } = render( const mockAreaProduct = new Product(
<Provider store={store}> 100,
<ProductCalculatorSelector /> { l: 4, w: 8, u: "ft" },
</Provider> {"name": "area product"},
);
const mockLengthProduct = new Product(
100,
{ l: 4, u: "ft" },
{"name": "length product"},
); );
expect(getByText('Please select a product')).toBeTruthy(); it('renders correctly', () => {
renderWithProviders(
(<ProductCalculatorSelector />),
{
products: [
mockAreaProduct.asObject,
mockLengthProduct.asObject,
],
}
)
expect(screen.getByText('Please select a product')).toBeTruthy();
const label = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`;
expect(screen.getByText(label)).toBeTruthy();
}); });
it('updates price when measurement is set', () => { it('a product can be selected', () => {
const { getByTestId, getByText } = render( renderWithProviders(
<Provider store={store}> (<ProductCalculatorSelector />),
<ProductCalculatorSelector /> {
</Provider> products: [
mockLengthProduct.asObject,
mockAreaProduct.asObject,
]
}
); );
// Assume there is a product with a priceFor function that returns 100 expect(screen.getByText('Please select a product')).toBeTruthy();
const product = { const areaLabel = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`;
priceFor: jest.fn().mockReturnValue(100), const lengthLabel = `${mockLengthProduct.attributes.name} (${mockLengthProduct.pricePerUnitDisplay})`;
};
// Assume there are units for measurement fireEvent.press(screen.getByText(areaLabel));
const units = ['cm', 'in']; const lengthInput = screen.getByLabelText("enter length");
const widthInput = screen.getByLabelText("enter length");
expect(lengthInput).toBeTruthy();
expect(widthInput).toBeTruthy();
// Assume there is a measurement input fireEvent.press(screen.getByText("in"));
const measurementInput = getByTestId('measurement-input');
// Simulate user input act(() => {
fireEvent.changeText(measurementInput, '10'); fireEvent.changeText(lengthInput, "2");
fireEvent.changeText(widthInput, "4");
});
// Check if the price has been updated jest.advanceTimersByTime(500);
expect(getByText('Price: 100')).toBeTruthy();
const price = mockAreaProduct.priceFor({l: 2, w: 4, u: "ft"});
const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,});
expect(screen.getByLabelText("calculated price").find().toBeTruthy();
}); });
}); });