diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index 2dac4b1..05d8d93 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -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
@@ -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)
},
})
diff --git a/apps/mobile/src/app/(tabs)/fertilizers.tsx b/apps/mobile/src/app/(tabs)/fertilizers.tsx
index 9a63356..bc076b2 100644
--- a/apps/mobile/src/app/(tabs)/fertilizers.tsx
+++ b/apps/mobile/src/app/(tabs)/fertilizers.tsx
@@ -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'
@@ -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 }>()
@@ -47,21 +45,6 @@ export function FertilizersScreen() {
const [includeDeletedItems, setIncludeDeletedItems] = React.useState(false)
const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300)
- useLayoutEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
- setFilterModalVisible(true)}
- color='#fff'
- isPulsating={!!searchQuery.trim()}
- testID='fertilizers-filter-button'
- />
-
- ),
- })
- }, [navigation, searchQuery])
-
const [addFormData, setAddFormData] = React.useState({
name: '',
type: fertilizerTypes[0],
@@ -337,6 +320,14 @@ export function FertilizersScreen() {
setFilterModalVisible(true)}
+ color={palette.brandPrimary}
+ isPulsating={!!searchQuery.trim()}
+ testID='fertilizers-filter-button'
+ />
+ }
>
Fertilizers
diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx
index f4941d7..4bbd553 100644
--- a/apps/mobile/src/app/(tabs)/index.tsx
+++ b/apps/mobile/src/app/(tabs)/index.tsx
@@ -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'
@@ -26,7 +26,6 @@ import { palette, styles } from '../../styles'
function ToDoScreen() {
const router = useRouter()
- const navigation = useNavigation()
const { alert } = useAlert()
const [checkedIds, setCheckedIds] = useState>(new Set())
const [modalVisible, setModalVisible] = useState(false)
@@ -40,21 +39,6 @@ function ToDoScreen() {
const [includeDeletedItems, setIncludeDeletedItems] = useState(false)
const debouncedSearchQuery = useDebounce(searchQuery.trim(), 300)
- useLayoutEffect(() => {
- navigation.setOptions({
- headerRight: () => (
-
- setFilterModalVisible(true)}
- color='#fff'
- isPulsating={!!searchQuery.trim()}
- testID='todolist-filter-button'
- />
-
- ),
- })
- }, [navigation, searchQuery])
-
const {
data,
isLoading,
@@ -387,12 +371,20 @@ function ToDoScreen() {
})
return (
-
-
- To Do List
-
+
+ setFilterModalVisible(true)}
+ color={palette.brandPrimary}
+ isPulsating={!!searchQuery.trim()}
+ testID='todolist-filter-button'
+ />
+ }
+ >
+ To Do List
+
{(isLoading && (
{
- navigation.setOptions({
- headerRight: () => (
-
- setFilterModalVisible(true)}
- color='#fff'
- isPulsating={!!searchQuery.trim()}
- testID='plants-filter-button'
- />
-
- ),
- })
- }, [navigation, searchQuery])
-
const {
data,
isLoading,
@@ -171,6 +154,14 @@ export function PlantsScreen() {
setFilterModalVisible(true)}
+ color={palette.brandPrimary}
+ isPulsating={!!searchQuery.trim()}
+ testID='plants-filter-button'
+ />
+ }
>
Plants
diff --git a/apps/mobile/src/components/__tests__/MinimumRequiredVersionGate.test.tsx b/apps/mobile/src/components/__tests__/MinimumRequiredVersionGate.test.tsx
index 6d45301..965a494 100644
--- a/apps/mobile/src/components/__tests__/MinimumRequiredVersionGate.test.tsx
+++ b/apps/mobile/src/components/__tests__/MinimumRequiredVersionGate.test.tsx
@@ -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
diff --git a/cypress/e2e/end-to-end/plants-search-filter-e2e.cy.ts b/cypress/e2e/end-to-end/plants-search-filter-e2e.cy.ts
index bc75351..c012ab7 100644
--- a/cypress/e2e/end-to-end/plants-search-filter-e2e.cy.ts
+++ b/cypress/e2e/end-to-end/plants-search-filter-e2e.cy.ts
@@ -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'
@@ -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',
@@ -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 = 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)
@@ -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')
diff --git a/scripts/testApiServer.ts b/scripts/testApiServer.ts
index ef82ddb..8653161 100644
--- a/scripts/testApiServer.ts
+++ b/scripts/testApiServer.ts
@@ -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'