Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { debugEndpoints } from './middlewares/debugEndpoints'
import { cronRouter } from './routers/cron'
import { trpcRouter } from './routers/trpc'

import { isTest } from './utils/isTest'

const app = express()
const PORT = config.api.port

Expand Down Expand Up @@ -50,6 +52,8 @@ app.use(
router: trpcRouter,
createContext: ({ req }) => ({ req }),
onError: ({ error, path }) => {
if (isTest) return

console.error(`❌ [TRPC Error on ${path}]`, error, error.cause)
},
})
Expand Down
27 changes: 9 additions & 18 deletions apps/mobile/src/app/(tabs)/fertilizers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react'
import { KeyboardAvoidingView, Modal, Platform, ScrollView, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'
import { useLayoutEffect } from 'react'
import { useLocalSearchParams, useNavigation } from 'expo-router'
import { useLocalSearchParams } from 'expo-router'
import { keepPreviousData } from '@tanstack/react-query'

import type { FertilizerType } from '@plannting/api/dist/models/Fertilizer'
Expand Down Expand Up @@ -30,7 +29,6 @@ import { palette, styles } from '../../styles'
const fertilizerTypes: FertilizerType[] = ['granulesOrPellets', 'liquid', 'powder', 'spike']

export function FertilizersScreen() {
const navigation = useNavigation()
const { alert } = useAlert()
const { expandFertilizerId } = useLocalSearchParams<{ expandFertilizerId?: string }>()

Expand All @@ -47,21 +45,6 @@ export function FertilizersScreen() {
const [includeDeletedItems, setIncludeDeletedItems] = React.useState(false)
const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300)

useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<View style={{ marginRight: 8 }}>
<IconFilter
onPress={() => setFilterModalVisible(true)}
color='#fff'
isPulsating={!!searchQuery.trim()}
testID='fertilizers-filter-button'
/>
</View>
),
})
}, [navigation, searchQuery])

const [addFormData, setAddFormData] = React.useState({
name: '',
type: fertilizerTypes[0],
Expand Down Expand Up @@ -337,6 +320,14 @@ export function FertilizersScreen() {
<ScreenWrapper onRefresh={refetch} scrollViewRef={scrollViewRef}>
<ScreenTitle
isLoading={isRefetching}
buttons={
<IconFilter
onPress={() => setFilterModalVisible(true)}
color={palette.brandPrimary}
isPulsating={!!searchQuery.trim()}
testID='fertilizers-filter-button'
/>
}
>
Fertilizers
</ScreenTitle>
Expand Down
40 changes: 16 additions & 24 deletions apps/mobile/src/app/(tabs)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Text, View, ScrollView, TouchableOpacity, Modal, Switch, StyleSheet } from 'react-native'
import { useNavigation, useRouter } from 'expo-router'
import { useRouter } from 'expo-router'
import { keepPreviousData } from '@tanstack/react-query'

import { Checkbox } from '../../components/Checkbox'
Expand All @@ -26,7 +26,6 @@ import { palette, styles } from '../../styles'

function ToDoScreen() {
const router = useRouter()
const navigation = useNavigation()
const { alert } = useAlert()
const [checkedIds, setCheckedIds] = useState<Set<string>>(new Set())
const [modalVisible, setModalVisible] = useState(false)
Expand All @@ -40,21 +39,6 @@ function ToDoScreen() {
const [includeDeletedItems, setIncludeDeletedItems] = useState(false)
const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300)

useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<View style={{ marginRight: 8 }}>
<IconFilter
onPress={() => setFilterModalVisible(true)}
color='#fff'
isPulsating={!!searchQuery.trim()}
testID='todolist-filter-button'
/>
</View>
),
})
}, [navigation, searchQuery])

const {
data,
isLoading,
Expand Down Expand Up @@ -387,12 +371,20 @@ function ToDoScreen() {
})

return (
<ScreenWrapper onRefresh={refetch}>
<ScreenTitle
isLoading={isRefetching}
>
To Do List
</ScreenTitle>
<ScreenWrapper onRefresh={refetch}>
<ScreenTitle
isLoading={isRefetching}
buttons={
<IconFilter
onPress={() => setFilterModalVisible(true)}
color={palette.brandPrimary}
isPulsating={!!searchQuery.trim()}
testID='todolist-filter-button'
/>
}
>
To Do List
</ScreenTitle>

{(isLoading && (
<LoadingSkeleton
Expand Down
27 changes: 9 additions & 18 deletions apps/mobile/src/app/(tabs)/plants.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from 'react'
import { Image as RNImage, ScrollView, StyleSheet, Switch, Text, TouchableOpacity, View } from 'react-native'
import { useLayoutEffect } from 'react'
import { Ionicons } from '@expo/vector-icons'
import { useLocalSearchParams, useNavigation, useRouter } from 'expo-router'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { keepPreviousData } from '@tanstack/react-query'

import { FloatingActionButton } from '../../components/FloatingActionButton'
Expand All @@ -29,7 +28,6 @@ import { palette, styles } from '../../styles'

export function PlantsScreen() {
const router = useRouter()
const navigation = useNavigation()
const { alert } = useAlert()
const { token } = useAuth()
const apiBaseUrl = config.api.baseUrl
Expand All @@ -41,21 +39,6 @@ export function PlantsScreen() {
const [includeDeletedItems, setIncludeDeletedItems] = React.useState(false)
const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300)

useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<View style={{ marginRight: 8 }}>
<IconFilter
onPress={() => setFilterModalVisible(true)}
color='#fff'
isPulsating={!!searchQuery.trim()}
testID='plants-filter-button'
/>
</View>
),
})
}, [navigation, searchQuery])

const {
data,
isLoading,
Expand Down Expand Up @@ -171,6 +154,14 @@ export function PlantsScreen() {
<ScreenWrapper onRefresh={refetch} scrollViewRef={scrollViewRef}>
<ScreenTitle
isLoading={isRefetching}
buttons={
<IconFilter
onPress={() => setFilterModalVisible(true)}
color={palette.brandPrimary}
isPulsating={!!searchQuery.trim()}
testID='plants-filter-button'
/>
}
>
Plants
</ScreenTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ jest.mock('../../trpc', () => ({
},
}))

// Mock ScreenWrapper so tests don't need SafeAreaProvider (it uses useSafeAreaInsets)
jest.mock('../ScreenWrapper', () => {
const React = require('react')
const { View } = require('react-native')

return {
ScreenWrapper: ({ children }: { children: React.ReactNode }) => React.createElement(View, null, children),
}
})

describe('MinimumRequiredVersionGate', () => {
let queryClient: QueryClient

Expand Down
39 changes: 16 additions & 23 deletions cypress/e2e/end-to-end/plants-search-filter-e2e.cy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
createTrpcClient,
trpcMutation,
} from '../utils/trpc'
import { trpcMutation } from '../utils/trpc'

// Mobile app URL - adjust if your Expo web app runs on a different port
const MOBILE_APP_URL = process.env.CYPRESS_MOBILE_APP_URL || 'http://localhost:8081'
Expand All @@ -17,9 +14,7 @@ describe('Mobile flow: Plants screen search filter (E2E UI)', () => {
path: 'auth.createAccount',
input: { name: 'Cypress User', email, password },
}).then(({ token }) => {
const trpc = createTrpcClient(token)

// Create multiple plants with different names
// Create multiple plants with different names (chain so all complete before continuing)
const plantNames = [
'Tomato Plant',
'Basil Herb',
Expand All @@ -28,19 +23,20 @@ describe('Mobile flow: Plants screen search filter (E2E UI)', () => {
'Carrot',
]

// Create all plants
cy.wrap(plantNames).each((plantName: string) => {
trpcMutation<{ _id: string }>({
path: 'plants.create',
input: { name: plantName },
headers: {
authorization: `Bearer ${token}`,
},
})
}).then(() => {
// Wait a moment for all plants to be created
cy.wait(500)
let createChain: Cypress.Chainable<unknown> = cy.wrap(undefined)
plantNames.forEach((plantName) => {
createChain = createChain.then(() =>
trpcMutation<{ _id: string }>({
path: 'plants.create',
input: { name: plantName },
headers: {
authorization: `Bearer ${token}`,
},
})
)
})

createChain.then(() => {
// Step 2: Visit the mobile app
cy.visit(MOBILE_APP_URL)

Expand Down Expand Up @@ -72,12 +68,9 @@ describe('Mobile flow: Plants screen search filter (E2E UI)', () => {
cy.get('[data-testid="plants-filter-button"]').click()

// Wait for the filter modal to appear - look for the search input
// The InputSearch component renders a TextInput which becomes an input in web
cy.get('[data-testid="search-input"]', { timeout: 5000 }).should('be.visible')

// The search input in the modal - InputSearch component renders as a TextInput which becomes an input in web
// We'll use a more specific selector that finds inputs with "Search" placeholder or inputs in the modal
const searchInputSelector = 'input[placeholder*="Search" i], input[placeholder*="search" i]'
const searchInputSelector = '[data-testid="search-input"]'

// Step 6: Test 1 - Search for "Tomato" - should return 2 plants
cy.get(searchInputSelector).clear().type('Tomato')
Expand Down
3 changes: 3 additions & 0 deletions scripts/testApiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ async function main() {
const mongo = await MongoMemoryServer.create()
const uri = mongo.getUri()

// Set NODE_ENV to test
process.env.NODE_ENV = 'test'

// Provide env required by the API
process.env.MONGO_URI = uri
process.env.JWT_SECRET = process.env.JWT_SECRET || 'cypress-test-secret'
Expand Down
Loading