diff --git a/src/components/CowCard/CowCard.js b/src/components/CowCard/CowCard.js index 8061cb8f6..c290ff6f0 100644 --- a/src/components/CowCard/CowCard.js +++ b/src/components/CowCard/CowCard.js @@ -1,3 +1,8 @@ +/** @typedef {import('../../handlers/ui-events')['default']} farmhand.uiHandlers */ +/** @typedef {import('../../components/Farmhand/Farmhand').farmhand.state} farmhand.state */ +/** @typedef {import('../../components/Farmhand/Farmhand').default} Farmhand */ +/** @typedef {import('../../index').farmhand.cow} farmhand.cow */ +/** @typedef {import('../../index').farmhand.cowBreedingPen} farmhand.cowBreedingPen */ import React, { useEffect, useRef, useState } from 'react' import { array, bool, func, number, object, string } from 'prop-types' @@ -35,42 +40,78 @@ const genderIcons = { [genders.MALE]: faMars, } -export const CowCard = ({ - allowCustomPeerCowNames, - cow, - cowBreedingPen, - cowIdOfferedForTrade, - cowInventory, - debounced, - handleCowAutomaticHugChange, - handleCowBreedChange, - handleCowHugClick, - handleCowOfferClick, - handleCowPurchaseClick, - handleCowWithdrawClick, - handleCowSellClick, - handleCowTradeClick, - id, - inventory, - isCowOfferedForTradeByPeer, - isSelected, - isOnline, - money, - purchasedCowPen, +/** + * @typedef {{ + * allowCustomPeerCowNames: boolean, + * cow: farmhand.cow, + * cowBreedingPen: farmhand.cowBreedingPen, + * cowInventory: farmhand.state['cowInventory'], + * cowIdOfferedForTrade: farmhand.cow['id'], + * debounced: Farmhand['handlers']['debounced'], + * handleCowAutomaticHugChange: farmhand.uiHandlers['handleCowAutomaticHugChange'], + * handleCowBreedChange: farmhand.uiHandlers['handleCowBreedChange'], + * handleCowHugClick: farmhand.uiHandlers['handleCowHugClick'], + * handleCowOfferClick: farmhand.uiHandlers['handleCowOfferClick'], + * handleCowPurchaseClick: farmhand.uiHandlers['handleCowPurchaseClick'], + * handleCowWithdrawClick: farmhand.uiHandlers['handleCowWithdrawClick'], + * handleCowSellClick: farmhand.uiHandlers['handleCowSellClick'], + * handleCowTradeClick: farmhand.uiHandlers['handleCowTradeClick'], + * id: farmhand.state['id'], + * inventory: farmhand.state['inventory'], + * isCowOfferedForTradeByPeer: boolean, + * isSelected: boolean, + * isOnline: boolean, + * money: farmhand.state['money'], + * purchasedCowPen: farmhand.state['purchasedCowPen'], + * huggingMachinesRemain?: boolean, + * }} CowCardProps + */ - huggingMachinesRemain = areHuggingMachinesInInventory(inventory), -}) => { +export const CowCard = ( + /** + * @type CowCardProps + */ + { + allowCustomPeerCowNames, + cow, + cowBreedingPen, + cowIdOfferedForTrade, + cowInventory, + debounced, + handleCowAutomaticHugChange, + handleCowBreedChange, + handleCowHugClick, + handleCowOfferClick, + handleCowPurchaseClick, + handleCowWithdrawClick, + handleCowSellClick, + handleCowTradeClick, + id, + inventory, + isCowOfferedForTradeByPeer, + isSelected, + isOnline, + money, + purchasedCowPen, + + huggingMachinesRemain = areHuggingMachinesInInventory(inventory), + } +) => { const cowDisplayName = getCowDisplayName(cow, id, allowCustomPeerCowNames) const [displayName, setDisplayName] = useState(cowDisplayName) const [cowImage, setCowImage] = useState(pixel) - const cardRef = useRef() - const scrollAnchorRef = useRef() + + // @see https://github.com/microsoft/TypeScript/issues/27387#issuecomment-659671940 + const cardRef = useRef(/** @type {Element|null} */ (null)) + const scrollAnchorRef = useRef(/** @type {HTMLAnchorElement|null} */ (null)) const isCowPurchased = !!cowInventory.find(({ id }) => id === cow.id) && !isCowOfferedForTradeByPeer - const cowValue = getCowValue(cow, isCowPurchased) + + // cow.originalOwnerId is only an empty string when it is for sale. + const cowValue = getCowValue(cow, cow.originalOwnerId !== '') const cowCanBeTradedAway = isOnline && !isCowInBreedingPen(cow, cowBreedingPen) const canCowBeTradedFor = Boolean( @@ -181,7 +222,7 @@ export const CowCard = ({ disabled: cowValue > money || cowInventory.length >= - PURCHASEABLE_COW_PENS.get(purchasedCowPen).cows, + (PURCHASEABLE_COW_PENS.get(purchasedCowPen)?.cows ?? 0), onClick: () => handleCowPurchaseClick(cow), variant: 'contained', }} @@ -308,6 +349,9 @@ CowCard.propTypes = { purchasedCowPen: number.isRequired, } +/** + * @param {CowCardProps} props + */ export default function Consumer(props) { return ( diff --git a/src/components/CowCard/Subheader/Subheader.js b/src/components/CowCard/Subheader/Subheader.js index 1ffb2b6ac..f65443877 100644 --- a/src/components/CowCard/Subheader/Subheader.js +++ b/src/components/CowCard/Subheader/Subheader.js @@ -1,3 +1,6 @@ +/** @typedef {import('../../../components/Farmhand/Farmhand').farmhand.state} farmhand.state */ +/** @typedef {import('../../../index').farmhand.cow} farmhand.cow */ +/** @typedef {import('../CowCard').CowCardProps} CowCardProps */ import React from 'react' import { array, bool, func, object, string } from 'prop-types' import Checkbox from '@material-ui/core/Checkbox' @@ -26,29 +29,58 @@ import './Subheader.sass' // The extra 0.5 is for rounding up to the next full heart. This allows a fully // happy cow to have full hearts on the beginning of a new day. +/** + * @param {number} heartIndex + * @param {number} numberOfFullHearts + */ const isHeartFull = (heartIndex, numberOfFullHearts) => heartIndex + 0.5 < numberOfFullHearts -const getCowMapById = memoize(cowInventory => - cowInventory.reduce((acc, cow) => { - acc[cow.id] = cow - return acc - }, {}) +const getCowMapById = memoize( + /** + * @param {farmhand.state['cowInventory']} cowInventory + */ + cowInventory => + cowInventory.reduce((acc, cow) => { + acc[cow.id] = cow + return acc + }, {}) ) -const Subheader = ({ - canCowBeTradedFor, - cow, - cowBreedingPen, - cowIdOfferedForTrade, - cowInventory, - cowValue, - handleCowAutomaticHugChange, - handleCowBreedChange, - huggingMachinesRemain, - id: playerId, - isCowPurchased, -}) => { +/** + * @typedef {Pick< + * CowCardProps, + * 'cow' | + * 'cowBreedingPen' | + * 'cowIdOfferedForTrade' | + * 'cowInventory' | + * 'handleCowAutomaticHugChange' | + * 'handleCowBreedChange' | + * 'huggingMachinesRemain' | + * 'id' + * > & { + * canCowBeTradedFor: boolean, + * cowValue: number, + * isCowPurchased: boolean, + * }} SubheaderProps + */ + +const Subheader = ( + /** @type {SubheaderProps} */ + { + canCowBeTradedFor, + cow, + cowBreedingPen, + cowIdOfferedForTrade, + cowInventory, + cowValue, + handleCowAutomaticHugChange, + handleCowBreedChange, + huggingMachinesRemain, + id: playerId, + isCowPurchased, + } +) => { const numberOfFullHearts = cow.happiness * 10 const isInBreedingPen = isCowInBreedingPen(cow, cowBreedingPen) const isRoomInBreedingPen = @@ -56,7 +88,7 @@ const Subheader = ({ const isThisCowOfferedForTrade = cowIdOfferedForTrade === cow.id const mateId = cowBreedingPen.cowId1 ?? cowBreedingPen.cowId2 - const mate = getCowMapById(cowInventory)[mateId] + const mate = getCowMapById(cowInventory)[mateId ?? ''] const isEligibleToBreed = cow.gender !== mate?.gender const canBeMovedToBreedingPen = @@ -80,7 +112,9 @@ const Subheader = ({ )}

Color: {COW_COLOR_NAMES[cow.color]}

- {isCowPurchased ? 'Value' : 'Price'}: {moneyString(cowValue)} + {/* cow.originalOwnerId is only an empty string when it is for sale. */} + {cow.originalOwnerId === '' ? 'Price' : 'Value'}:{' '} + {moneyString(cowValue)}

Weight: {getCowWeight(cow)} lbs.

{(isCowPurchased || canCowBeTradedFor) && ( diff --git a/src/components/CowCard/Subheader/Subheader.test.js b/src/components/CowCard/Subheader/Subheader.test.js index 822967823..f04dd7639 100644 --- a/src/components/CowCard/Subheader/Subheader.test.js +++ b/src/components/CowCard/Subheader/Subheader.test.js @@ -1,11 +1,13 @@ import React from 'react' -import { render } from '@testing-library/react' +import { render, screen } from '@testing-library/react' -import { generateCow } from '../../../utils' -import { cowColors } from '../../../enums' +import { getCowStub } from '../../../test-utils/stubs/cowStub' +import { moneyString } from '../../../utils/moneyString' import Subheader from './Subheader' +const COW_VALUE = 1000 + describe('Subheader', () => { let baseProps @@ -13,16 +15,11 @@ describe('Subheader', () => { jest.spyOn(Math, 'random').mockReturnValue(0) baseProps = { canCowBeTradedFor: false, - cow: generateCow({ - color: cowColors.WHITE, - happiness: 0, - name: '', - baseWeight: 100, - }), + cow: getCowStub(), cowBreedingPen: { cowId1: null, cowId2: null, daysUntilBirth: -1 }, cowIdOfferedForTrade: '', cowInventory: [], - cowValue: 1000, + cowValue: COW_VALUE, huggingMachinesRemain: false, id: '', isCowPurchased: false, @@ -52,11 +49,8 @@ describe('Subheader', () => { { { }) }) }) + + describe('price/value display', () => { + test('displays price for cows that can be purchased', () => { + render( + + ) + + const price = screen.getByText(`Price: ${moneyString(COW_VALUE)}`) + expect(price).toBeInTheDocument() + }) + + test('displays value for cows that have been purchased', () => { + render( + + ) + + const value = screen.getByText(`Value: ${moneyString(COW_VALUE)}`) + expect(value).toBeInTheDocument() + }) + }) }) diff --git a/src/handlers/ui-events.js b/src/handlers/ui-events.js index a21269717..279692718 100644 --- a/src/handlers/ui-events.js +++ b/src/handlers/ui-events.js @@ -1,6 +1,7 @@ /** * @typedef {import("../index").farmhand.item} item * @typedef {import("../index").farmhand.keg} keg + * @typedef {import("../index").farmhand.cow} farmhand.cow */ import { saveAs } from 'file-saver' import window from 'global/window' @@ -110,7 +111,7 @@ export default { }, /** - * @param {external:React.SyntheticEvent} e + * @param {React.SyntheticEvent} e * @param {farmhand.cow} cow */ handleCowAutomaticHugChange({ target: { checked } }, cow) { @@ -118,7 +119,7 @@ export default { }, /** - * @param {external:React.SyntheticEvent} e + * @param {React.SyntheticEvent} e * @param {farmhand.cow} cow */ handleCowBreedChange({ target: { checked } }, cow) { @@ -147,7 +148,7 @@ export default { }, /** - * @param {external:React.SyntheticEvent} e + * @param {React.SyntheticEvent} e * @param {farmhand.cow} cow */ handleCowNameInputChange({ target: { value } }, cow) { @@ -163,7 +164,7 @@ export default { }, /** - * @param {external:React.SyntheticEvent} e + * @param {React.SyntheticEvent} e */ handleViewChange({ target: { value } }) { this.setState({ stageFocus: value }) diff --git a/src/test-utils/stubs/cowStub.js b/src/test-utils/stubs/cowStub.js index c64bb3b1b..ae999f352 100644 --- a/src/test-utils/stubs/cowStub.js +++ b/src/test-utils/stubs/cowStub.js @@ -1,30 +1,15 @@ /** @typedef {import('../../index').farmhand.cow} farmhand.cow */ -import { v4 as uuid } from 'uuid' +import { generateCow } from '../../utils' -import { cowColors, genders } from '../../enums' - -export const getCowStub = () => { - /** @type farmhand.cow */ - const cow = { +/** + * @param {Partial?} overrides + */ +export const getCowStub = (overrides = {}) => { + const cow = generateCow({ baseWeight: 1000, - color: cowColors.BLUE, - colorsInBloodline: {}, - daysOld: 1, - daysSinceMilking: 0, - daysSinceProducingFertilizer: 0, - gender: genders.FEMALE, - happiness: 0, - happinessBoostsToday: 0, - id: uuid(), - isBred: false, - isUsingHuggingMachine: false, - name: '', - originalOwnerId: uuid(), - ownerId: uuid(), - timesTraded: 0, - weightMultiplier: 1, - } + ...overrides, + }) return cow } diff --git a/src/utils/index.js b/src/utils/index.js index 966db1c07..5b2a44370 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -763,12 +763,12 @@ export const inventorySpaceRemaining = ({ inventory, inventoryLimit }) => export const doesInventorySpaceRemain = state => inventorySpaceRemaining(state) > 0 -/** - * @param {Array.} inventory - * @return {boolean} - */ -export const areHuggingMachinesInInventory = memoize(inventory => - inventory.some(({ id }) => id === HUGGING_MACHINE_ITEM_ID) +export const areHuggingMachinesInInventory = memoize( + /** + * @param {farmhand.state['inventory']} inventory + * @return {boolean} + */ + inventory => inventory.some(({ id }) => id === HUGGING_MACHINE_ITEM_ID) ) /**