complete more of the unit tests.
This commit is contained in:
parent
76fe4eb34a
commit
379f43dcd9
@ -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>
|
||||||
)
|
)
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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' });
|
||||||
});
|
});
|
||||||
|
@ -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', () => {
|
||||||
|
|
||||||
|
const mockAreaProduct = new Product(
|
||||||
|
100,
|
||||||
|
{ l: 4, w: 8, u: "ft" },
|
||||||
|
{"name": "area product"},
|
||||||
|
);
|
||||||
|
const mockLengthProduct = new Product(
|
||||||
|
100,
|
||||||
|
{ l: 4, u: "ft" },
|
||||||
|
{"name": "length product"},
|
||||||
|
);
|
||||||
|
|
||||||
it('renders correctly', () => {
|
it('renders correctly', () => {
|
||||||
const { getByText } = render(
|
renderWithProviders(
|
||||||
<Provider store={store}>
|
(<ProductCalculatorSelector />),
|
||||||
<ProductCalculatorSelector />
|
{
|
||||||
</Provider>
|
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('a product can be selected', () => {
|
||||||
|
renderWithProviders(
|
||||||
|
(<ProductCalculatorSelector />),
|
||||||
|
{
|
||||||
|
products: [
|
||||||
|
mockLengthProduct.asObject,
|
||||||
|
mockAreaProduct.asObject,
|
||||||
|
]
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByText('Please select a product')).toBeTruthy();
|
expect(screen.getByText('Please select a product')).toBeTruthy();
|
||||||
|
const areaLabel = `${mockAreaProduct.attributes.name} (${mockAreaProduct.pricePerUnitDisplay})`;
|
||||||
|
const lengthLabel = `${mockLengthProduct.attributes.name} (${mockLengthProduct.pricePerUnitDisplay})`;
|
||||||
|
|
||||||
|
fireEvent.press(screen.getByText(areaLabel));
|
||||||
|
const lengthInput = screen.getByLabelText("enter length");
|
||||||
|
const widthInput = screen.getByLabelText("enter length");
|
||||||
|
expect(lengthInput).toBeTruthy();
|
||||||
|
expect(widthInput).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.press(screen.getByText("in"));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.changeText(lengthInput, "2");
|
||||||
|
fireEvent.changeText(widthInput, "4");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates price when measurement is set', () => {
|
jest.advanceTimersByTime(500);
|
||||||
const { getByTestId, getByText } = render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<ProductCalculatorSelector />
|
|
||||||
</Provider>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assume there is a product with a priceFor function that returns 100
|
const price = mockAreaProduct.priceFor({l: 2, w: 4, u: "ft"});
|
||||||
const product = {
|
const sPrice = price.toLocaleString(undefined, {maximumFractionDigits: 2, minimumFractionDigits: 2,});
|
||||||
priceFor: jest.fn().mockReturnValue(100),
|
expect(screen.getByLabelText("calculated price").find().toBeTruthy();
|
||||||
};
|
|
||||||
|
|
||||||
// Assume there are units for measurement
|
|
||||||
const units = ['cm', 'in'];
|
|
||||||
|
|
||||||
// Assume there is a measurement input
|
|
||||||
const measurementInput = getByTestId('measurement-input');
|
|
||||||
|
|
||||||
// Simulate user input
|
|
||||||
fireEvent.changeText(measurementInput, '10');
|
|
||||||
|
|
||||||
// Check if the price has been updated
|
|
||||||
expect(getByText('Price: 100')).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user