attempt to fix unit tests.
This commit is contained in:
@ -3,15 +3,15 @@ import { useState } from "react";
|
||||
import { Button, StyleSheet, Text, TextInput, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
export type t_length_unit = "foot" | "inch"
|
||||
export type mode = "length" | "area"
|
||||
|
||||
export type LengthInputProps = {
|
||||
onLengthSet?: (length: Measure<"length">) => any,
|
||||
onAreaSet?: (area: Measure<"area">) => any,
|
||||
onMeasurementSet?: (length: Measure<"length" | "area">) => any,
|
||||
isArea?: boolean,
|
||||
}
|
||||
|
||||
export type t_length_unit = "foot" | "inch"
|
||||
|
||||
export function LengthInput(props: LengthInputProps) {
|
||||
export function MeasurementInput(props: LengthInputProps) {
|
||||
|
||||
const [length, setLength] = useState(null as null | number);
|
||||
const [width, setWidth] = useState(null as null | number);
|
||||
@ -22,11 +22,11 @@ export function LengthInput(props: LengthInputProps) {
|
||||
setLength(value);
|
||||
if (!props.isArea) {
|
||||
const len = en_length(unit, value)
|
||||
props.onLengthSet && props.onLengthSet(len)
|
||||
props.onMeasurementSet && props.onMeasurementSet(len)
|
||||
} else {
|
||||
const en_unit = unit == "foot" ? "squareFoot" : "squareInch"
|
||||
const ar = en_area(en_unit, value);
|
||||
props.onAreaSet && props.onAreaSet(ar);
|
||||
props.onMeasurementSet && props.onMeasurementSet(ar);
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export function LengthInput(props: LengthInputProps) {
|
||||
const value = parseFloat(text);
|
||||
setLength(value);
|
||||
const len = en_length(unit, value)
|
||||
props.onLengthSet && props.onLengthSet(len)
|
||||
props.onMeasurementSet && props.onMeasurementSet(len)
|
||||
}
|
||||
|
||||
return (
|
||||
|
45
components/ProductAttributeEditor.tsx
Normal file
45
components/ProductAttributeEditor.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Product } from "@/lib/product";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import React from "react";
|
||||
import { ChangeEvent, SetStateAction, useState } from "react";
|
||||
import { NativeSyntheticEvent, StyleSheet, Text, TextInput, TextInputChangeEventData, TextInputProps, TouchableHighlight, View } from "react-native";
|
||||
|
||||
export type ProductAttributeChangeFunc = (product_id: string, key: string, newValue: string) => any;
|
||||
export type ProductAttributeDeleteFunc = (product_id: string, key: string) => any;
|
||||
|
||||
export type ProductAttributeProps = { product: Product, key: string, value: string, onChange?: ProductAttributeChangeFunc, onDelete?: ProductAttributeChangeFunc, };
|
||||
|
||||
export default function ProductAttributeEditor({ product, key, value, onDelete, onChange }: ProductAttributeProps) {
|
||||
const [doEdit, setDoEdit] = useState(true);
|
||||
const [newValue, setNewValue] = useState(value);
|
||||
|
||||
const doChange = (e : any) => {
|
||||
setNewValue(e.target.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>
|
||||
{key}
|
||||
</Text>
|
||||
<View>
|
||||
<TouchableHighlight
|
||||
onPress={() => onDelete && onDelete(product.id, key, value)}
|
||||
aria-label="Delete Attribute">
|
||||
<Ionicons name="trash-bin-outline" />
|
||||
</TouchableHighlight>
|
||||
{doEdit ?
|
||||
(<Text>
|
||||
{newValue}
|
||||
</Text>) : (
|
||||
<TextInput value={newValue} onChange={doChange} />
|
||||
)
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
|
||||
});
|
47
components/ProductEditor.tsx
Normal file
47
components/ProductEditor.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/app/store"
|
||||
import { deleteProduct, selectProducts, updateProduct } from "@/features/product/productSlice"
|
||||
import { Product } from "@/lib/product";
|
||||
import { FlatListComponent, StyleSheet, Text } from "react-native";
|
||||
import { FlatList } from "react-native-reanimated/lib/typescript/Animated";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { ProductEditorItem } from "./ProductEditorItem";
|
||||
|
||||
export const ProductEditor = () => {
|
||||
const products = useAppSelector(selectProducts) as Product [];
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
function onProductDeleted(product_id: string) {
|
||||
dispatch(deleteProduct(product_id));
|
||||
}
|
||||
|
||||
function onProductUpdated(product_id: string, product: Product) {
|
||||
dispatch(updateProduct(product));
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<FlatList
|
||||
data={products}
|
||||
renderItem={
|
||||
({item}) => {
|
||||
return (
|
||||
<ProductEditorItem
|
||||
product={item}
|
||||
onProductDeleted={onProductDeleted}
|
||||
onProductUpdated={onProductUpdated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
product: {
|
||||
|
||||
}
|
||||
})
|
58
components/ProductEditorItem.tsx
Normal file
58
components/ProductEditorItem.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Product } from "@/lib/product"
|
||||
import { useState } from "react"
|
||||
import { StyleSheet, Text, TouchableHighlight } from "react-native"
|
||||
import { FlatList } from "react-native-reanimated/lib/typescript/Animated";
|
||||
import ProductAttributeEditor from "./ProductAttributeEditor";
|
||||
|
||||
export type ProductUpdatedFunc = (product_id : string, product : Product) => any;
|
||||
export type ProductDeletedFunc = (product_id : string) => any;
|
||||
|
||||
export type ProductEditorItemProps = {
|
||||
product : Product,
|
||||
onProductUpdated?: ProductUpdatedFunc,
|
||||
onProductDeleted?: ProductDeletedFunc,
|
||||
}
|
||||
|
||||
export const ProductEditorItem = ({ product, onProductUpdated, onProductDeleted }: ProductEditorItemProps) => {
|
||||
|
||||
const [showAttributes, setShowAttributes] = useState(true);
|
||||
|
||||
function onAttributeChanged(product_id: string, key: string, newValue: string) {
|
||||
product.attributes[key] = newValue;
|
||||
onProductUpdated && onProductUpdated(product_id, product);
|
||||
|
||||
}
|
||||
|
||||
function onAttributeDelete(product_id: string, key: string) {
|
||||
product.removeAttribute(key);
|
||||
onProductUpdated && onProductUpdated(product_id, product);
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={() => setShowAttributes(true)}
|
||||
>
|
||||
<Text style={styles.product}>{product.attributes.name || `Product ${product.id}`} </Text>
|
||||
{showAttributes &&
|
||||
(
|
||||
<FlatList
|
||||
data={product.attributesAsList}
|
||||
renderItem={({ item }) => (
|
||||
<ProductAttributeEditor
|
||||
product={product}
|
||||
key={item.key}
|
||||
value={item.value}
|
||||
onChange={onAttributeChanged}
|
||||
onDelete={onAttributeDelete}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</TouchableHighlight>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
product: {},
|
||||
})
|
44
components/ProductTile.tsx
Normal file
44
components/ProductTile.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Product } from "@/lib/product"
|
||||
import { ImageBackground, StyleProp, StyleSheet, Text, ViewStyle } from "react-native";
|
||||
import { AnimatedStyle } from "react-native-reanimated";
|
||||
import { View } from "react-native-reanimated/lib/typescript/Animated";
|
||||
|
||||
export type OnProductSelectedFunc = (product : Product) => any;
|
||||
|
||||
type MyStyle = StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
|
||||
|
||||
export type ProductTileProps = {
|
||||
product: (Product),
|
||||
onProductSelected?: OnProductSelectedFunc,
|
||||
style?: {
|
||||
tile?: MyStyle,
|
||||
image?: MyStyle,
|
||||
}
|
||||
}
|
||||
|
||||
const FALLBACK_IMAGE = "";
|
||||
|
||||
export function ProductTile ({product, onProductSelected, style} : ProductTileProps) {
|
||||
const src = product.attributes.image || FALLBACK_IMAGE;
|
||||
return (
|
||||
<View style={style?.tile}>
|
||||
<ImageBackground
|
||||
src={src}
|
||||
resizeMode="cover"
|
||||
style={styles.image}
|
||||
>
|
||||
<Text style={styles.text}>{product.attributes.name || `Product ${product.id}`}</Text>
|
||||
<Text style={styles.text}>{ product.pricePerUnit.toString() } / {product.measurement.value} {product.measurement.unit.symbol} </Text>
|
||||
</ImageBackground>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
|
||||
},
|
||||
text: {
|
||||
|
||||
},
|
||||
})
|
27
components/__tests__/ProductAttributeEditor-test.tsx
Normal file
27
components/__tests__/ProductAttributeEditor-test.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { Product } from "@/lib/product"
|
||||
import ProductAttributeEditor from "../ProductAttributeEditor"
|
||||
import { renderWithProviders } from "./util"
|
||||
import { area } from "enheter"
|
||||
import {screen} from '@testing-library/react-native';
|
||||
import React from "react";
|
||||
|
||||
describe("Product editor tests", () => {
|
||||
it("Product attributes render", () => {
|
||||
const product = new Product(
|
||||
100,
|
||||
area("squareFoot", 4 * 7)
|
||||
);
|
||||
const onChange = jest.fn();
|
||||
const onDelete = jest.fn();
|
||||
renderWithProviders(
|
||||
<ProductAttributeEditor
|
||||
key="Name"
|
||||
value="product"
|
||||
product={product}
|
||||
onChange={onChange}
|
||||
onDelete={onDelete}
|
||||
/>);
|
||||
expect(screen.findByLabelText("Delete Attribute")).not.toBeNull();
|
||||
|
||||
})
|
||||
})
|
41
components/__tests__/util.tsx
Normal file
41
components/__tests__/util.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { RenderOptions, render } from "@testing-library/react-native";
|
||||
import { PropsWithChildren, ReactElement } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
import { setupStore, RootState } from "@/app/store";
|
||||
import { Price, Product } from "@/lib/product";
|
||||
import { area, length } from "enheter";
|
||||
|
||||
export interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
|
||||
preloadedState?: Partial<RootState>;
|
||||
store?: any; // TODO
|
||||
}
|
||||
|
||||
const basicState = {
|
||||
products: [
|
||||
new Product(20.00, length("foot", 5), {name: "Track"}),
|
||||
new Product(20.00, area("squareFoot", 5), {name: "Shelf"}),
|
||||
]
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: ReactElement,
|
||||
preloadedState = basicState,
|
||||
extendedRenderOptions: ExtendedRenderOptions = {},
|
||||
) {
|
||||
const {
|
||||
// Automatically create a store instance if no store was passed in
|
||||
store = setupStore(preloadedState as Partial<RootState>),
|
||||
...renderOptions
|
||||
} = extendedRenderOptions;
|
||||
|
||||
const Wrapper = ({ children }: PropsWithChildren) => (
|
||||
<Provider store={store}>{children}</Provider>
|
||||
);
|
||||
|
||||
// Return an object with the store and all of RTL's query functions
|
||||
return {
|
||||
store,
|
||||
...render(ui, { wrapper: Wrapper, ...renderOptions })
|
||||
};
|
||||
}
|
||||
|
Reference in New Issue
Block a user