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'