diff --git a/.eslintrc.js b/.eslintrc.js index e1e78994..943c15e1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -38,4 +38,44 @@ module.exports = { '*.config.js', '*.config.ts', ], + overrides: [ + { + files: ['**/*.cjs', 'scripts/**/*.js'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + files: [ + '**/__tests__/**/*.{ts,tsx}', + '**/*.test.{ts,tsx}', + '**/__mocks__/**/*.{ts,tsx}', + ], + env: { + jest: true, + }, + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + { + files: ['scripts/**/*.mjs'], + rules: { + 'no-console': 'off', + }, + }, + { + files: [ + 'tests/**/*.{ts,tsx}', + 'packages/shared-tests/**/*.{ts,tsx}', + 'apps/mobile/__tests__/**/*.{ts,tsx}', + 'scripts/**/*.js', + ], + rules: { + 'no-console': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + }, + }, + ], }; diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 915a2e8f..32b47d49 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,9 +25,9 @@ on: - '**/babel.config.*' - '**/metro.config.*' - '**/vite.config.*' - # Test-related scripts (if they change, tests might be affected) - - 'scripts/merge-coverage.js' - - 'scripts/format-ci-summary.mjs' + # Scripts and repo lint config (lint covers scripts/**) + - 'scripts/**' + - '.eslintrc.js' # Workflow file itself - '.github/workflows/test.yml' diff --git a/apps/mobile/__tests__/App.test.tsx b/apps/mobile/__tests__/App.test.tsx index 5a94c315..ce2e203a 100644 --- a/apps/mobile/__tests__/App.test.tsx +++ b/apps/mobile/__tests__/App.test.tsx @@ -103,7 +103,6 @@ jest.mock('@react-navigation/native', () => ({ })); jest.mock('@react-navigation/native-stack', () => { - const React = require('react'); return { createNativeStackNavigator: () => ({ Navigator: ({ children }: any) => children, @@ -131,7 +130,6 @@ jest.mock('@beakerstack/shared/components/auth/ProtectedRoute', () => ({ // Mock form components to avoid StyleSheet.create() native bridge issues in tests jest.mock('@beakerstack/shared/components/forms/FormInput.native', () => ({ FormInput: ({ label, value, onChange, ...props }: any) => { - const React = require('react'); const { TextInput, View, Text } = require('react-native'); return ( @@ -144,7 +142,6 @@ jest.mock('@beakerstack/shared/components/forms/FormInput.native', () => ({ jest.mock('@beakerstack/shared/components/forms/FormButton.native', () => ({ FormButton: ({ title, onPress, ...props }: any) => { - const React = require('react'); const { TouchableOpacity, Text } = require('react-native'); return ( @@ -156,13 +153,11 @@ jest.mock('@beakerstack/shared/components/forms/FormButton.native', () => ({ jest.mock('@beakerstack/shared/components/forms/FormError.native', () => ({ FormError: ({ message }: any) => { - const React = require('react'); const { Text } = require('react-native'); return message ? {message} : null; }, })); -import React from 'react'; import { render } from '@testing-library/react-native'; import { describe, it, expect } from '@jest/globals'; import App from '../App'; diff --git a/apps/mobile/__tests__/components/AvatarUpload.test.tsx b/apps/mobile/__tests__/components/AvatarUpload.test.tsx index 8da14c44..15c22aad 100644 --- a/apps/mobile/__tests__/components/AvatarUpload.test.tsx +++ b/apps/mobile/__tests__/components/AvatarUpload.test.tsx @@ -56,7 +56,7 @@ describe('AvatarUpload.native', () => { }); it('renders with current avatar URL', () => { - const { UNSAFE_getByType } = render( + render( ({ // Mock screens jest.mock('../../src/screens/HomeScreen', () => { - const React = require('react'); const { View, Text } = require('react-native'); return () => ( @@ -21,7 +18,6 @@ jest.mock('../../src/screens/HomeScreen', () => { }); jest.mock('../../src/screens/LoginScreen', () => { - const React = require('react'); const { View, Text } = require('react-native'); return () => ( @@ -31,7 +27,6 @@ jest.mock('../../src/screens/LoginScreen', () => { }); jest.mock('../../src/screens/SignupScreen', () => { - const React = require('react'); const { View, Text } = require('react-native'); return () => ( @@ -41,7 +36,6 @@ jest.mock('../../src/screens/SignupScreen', () => { }); jest.mock('../../src/screens/DashboardScreen', () => { - const React = require('react'); const { View, Text } = require('react-native'); return () => ( @@ -51,7 +45,6 @@ jest.mock('../../src/screens/DashboardScreen', () => { }); jest.mock('../../src/screens/ProfileScreen', () => { - const React = require('react'); const { View, Text } = require('react-native'); return () => ( @@ -84,7 +77,7 @@ describe('AppNavigator', () => { it('exposes navigation ref in dev mode', () => { const originalDev = __DEV__; - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = true; render(); @@ -92,14 +85,14 @@ describe('AppNavigator', () => { // Navigation ref should be exposed to global scope in dev mode expect((global as any).navigationRef).toBeDefined(); - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = originalDev; }); it.skip('does not expose navigation ref in production', () => { // Skip - __DEV__ is read-only in Jest environment const originalDev = __DEV__; - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = false; render(); @@ -107,7 +100,7 @@ describe('AppNavigator', () => { // Navigation ref should not be exposed in production expect((global as any).navigationRef).toBeUndefined(); - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = originalDev; }); }); diff --git a/apps/mobile/__tests__/screens/LoginScreen.test.tsx b/apps/mobile/__tests__/screens/LoginScreen.test.tsx index 06202cea..02d46007 100644 --- a/apps/mobile/__tests__/screens/LoginScreen.test.tsx +++ b/apps/mobile/__tests__/screens/LoginScreen.test.tsx @@ -256,7 +256,7 @@ describe('LoginScreen', () => { it('redirects to Dashboard if already authenticated', async () => { const mockClient = createMockSupabaseClient(true); - const { getByText } = renderWithProviders( + renderWithProviders( , mockClient ); diff --git a/apps/mobile/__tests__/screens/ProfileScreen.test.tsx b/apps/mobile/__tests__/screens/ProfileScreen.test.tsx index 380ec79c..673ea8de 100644 --- a/apps/mobile/__tests__/screens/ProfileScreen.test.tsx +++ b/apps/mobile/__tests__/screens/ProfileScreen.test.tsx @@ -93,7 +93,6 @@ jest.mock( '@beakerstack/shared/components/profile/ProfileHeader.native', () => ({ ProfileHeader: ({ profile }: any) => { - const React = require('react'); const { View, Text } = require('react-native'); return ( @@ -110,7 +109,6 @@ jest.mock( jest.mock('@beakerstack/shared/components/profile/ProfileStats.native', () => ({ ProfileStats: ({ profile }: any) => { - const React = require('react'); const { View, Text } = require('react-native'); return profile ? ( @@ -124,7 +122,6 @@ jest.mock( '@beakerstack/shared/components/profile/ProfileEditor.native', () => ({ ProfileEditor: ({ user }: any) => { - const React = require('react'); const { View, Text } = require('react-native'); return ( diff --git a/apps/mobile/__tests__/screens/SignupScreen.test.tsx b/apps/mobile/__tests__/screens/SignupScreen.test.tsx index 2a3b4672..63802709 100644 --- a/apps/mobile/__tests__/screens/SignupScreen.test.tsx +++ b/apps/mobile/__tests__/screens/SignupScreen.test.tsx @@ -300,7 +300,7 @@ describe('SignupScreen', () => { it('redirects to Dashboard if already authenticated', async () => { const mockClient = createMockSupabaseClient(true); - const { getByText } = renderWithProviders( + renderWithProviders( , mockClient ); diff --git a/apps/mobile/src/lib/__tests__/supabase.test.ts b/apps/mobile/src/lib/__tests__/supabase.test.ts index 3611a5ae..cee657ad 100644 --- a/apps/mobile/src/lib/__tests__/supabase.test.ts +++ b/apps/mobile/src/lib/__tests__/supabase.test.ts @@ -73,7 +73,6 @@ describe('supabase.ts', () => { (global as any).__DEV__ = false; Platform.OS = 'ios'; - // eslint-disable-next-line @typescript-eslint/no-var-requires const { supabase } = require('../supabase'); expect(mockCreateClient).toHaveBeenCalledWith( @@ -92,7 +91,6 @@ describe('supabase.ts', () => { }); it('should export supabase client', () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires const module = require('../supabase'); expect(module).toHaveProperty('supabase'); diff --git a/package.json b/package.json index e1035c51..d6f70a95 100644 --- a/package.json +++ b/package.json @@ -93,12 +93,13 @@ "test:coverage:shared": "cd packages/shared-tests && npm run test:coverage", "test:coverage:merge": "node scripts/merge-coverage.js", "test:unit:scripts": "node --test scripts/__tests__/rename-project.test.mjs scripts/__tests__/detect-repo-identity.test.mjs scripts/__tests__/setup-aws-discover.test.mjs scripts/__tests__/setup-dotenv.test.mjs scripts/__tests__/setup-full-logging.test.mjs scripts/__tests__/setup-manifest-github-sync.test.mjs scripts/__tests__/setup-secret-input.test.mjs scripts/__tests__/setup-supabase-pick-recommend.test.mjs && ./scripts/pr-preview/check-preview-deploy-needed.sh --self-test", - "lint": "npm run lint:mobile; npm run lint:web; npm run lint:shared", + "lint": "npm run lint:mobile; npm run lint:web; npm run lint:shared; npm run lint:repo", "lint:mobile": "eslint apps/mobile/src --ext ts,tsx --report-unused-disable-directives", "lint:web": "eslint apps/web/src --ext ts,tsx --report-unused-disable-directives", "lint:shared": "eslint packages/shared/src --ext ts,tsx --report-unused-disable-directives", - "lint:strict": "npm run lint:mobile -- --max-warnings 0 && npm run lint:web -- --max-warnings 0 && npm run lint:shared -- --max-warnings 0", - "lint:fix": "eslint apps/mobile/src apps/web/src packages/shared/src --ext ts,tsx --fix", + "lint:repo": "eslint scripts tests packages/shared-tests packages/test-utils/src apps/mobile/__tests__ --ext ts,tsx,js,mjs,cjs --report-unused-disable-directives", + "lint:strict": "npm run lint:mobile -- --max-warnings 0 && npm run lint:web -- --max-warnings 0 && npm run lint:shared -- --max-warnings 0 && npm run lint:repo -- --max-warnings 0", + "lint:fix": "eslint apps/mobile/src apps/web/src packages/shared/src scripts tests packages/shared-tests packages/test-utils/src apps/mobile/__tests__ --ext ts,tsx,js,mjs,cjs --fix", "pretype-check": "cd packages/shared && npm run build", "type-check": "npm run type-check:mobile; npm run type-check:web; npm run type-check:shared", "type-check:mobile": "cd apps/mobile && npx tsc --noEmit", diff --git a/packages/shared-tests/__mocks__/react-router-dom.tsx b/packages/shared-tests/__mocks__/react-router-dom.tsx index 452f2864..0cfc6033 100644 --- a/packages/shared-tests/__mocks__/react-router-dom.tsx +++ b/packages/shared-tests/__mocks__/react-router-dom.tsx @@ -12,7 +12,7 @@ export const MemoryRouter = ({ children }: { children: ReactNode }) => (
{children}
); -export const useNavigate = () => (path: string) => { +export const useNavigate = () => (_path: string) => { /* noop for tests */ }; diff --git a/packages/shared-tests/__tests__/components/forms/FormInput.native.test.tsx b/packages/shared-tests/__tests__/components/forms/FormInput.native.test.tsx index 8de712cc..5d28deff 100644 --- a/packages/shared-tests/__tests__/components/forms/FormInput.native.test.tsx +++ b/packages/shared-tests/__tests__/components/forms/FormInput.native.test.tsx @@ -37,9 +37,7 @@ describe('FormInput (Native)', () => { ); const input = screen.getByPlaceholderText('Enter username'); - // react-native-web renders TextInput - need to trigger onChangeText - // For react-native-web, we can use the native event - const nativeEvent = { target: { value: 'newvalue' } }; + // react-native-web renders TextInput - fire changeText directly fireEvent(input, 'changeText', 'newvalue'); expect(mockOnChange).toHaveBeenCalledWith('newvalue'); diff --git a/packages/shared-tests/__tests__/components/navigation/AppHeader.native.test.tsx b/packages/shared-tests/__tests__/components/navigation/AppHeader.native.test.tsx index a6d489c0..de7a97c7 100644 --- a/packages/shared-tests/__tests__/components/navigation/AppHeader.native.test.tsx +++ b/packages/shared-tests/__tests__/components/navigation/AppHeader.native.test.tsx @@ -31,9 +31,13 @@ jest.mock('react-native', () => { // Mock UserMenu jest.mock('@beakerstack/shared/components/navigation/UserMenu.native', () => ({ - UserMenu: ({ user, profile }: { user: any; profile: any }) => ( -
User Menu
- ), + UserMenu: ({ + user: _user, + profile: _profile, + }: { + user: any; + profile: any; + }) =>
User Menu
, })); // react-native-svg is mocked via moduleNameMapper diff --git a/packages/shared-tests/__tests__/components/navigation/AppHeader.test.tsx b/packages/shared-tests/__tests__/components/navigation/AppHeader.test.tsx index 4853cb97..98fc3dfa 100644 --- a/packages/shared-tests/__tests__/components/navigation/AppHeader.test.tsx +++ b/packages/shared-tests/__tests__/components/navigation/AppHeader.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; -import { BrowserRouter, MemoryRouter } from 'react-router-dom'; +import { BrowserRouter } from 'react-router-dom'; import '@testing-library/jest-dom'; import { AppHeader } from '@beakerstack/shared/components/navigation/AppHeader.web'; import { AuthProvider } from '@beakerstack/shared/contexts/AuthContext'; @@ -27,22 +27,6 @@ const createMockSupabaseClient = (): SupabaseClient => { } as unknown as SupabaseClient; }; -const renderWithProviders = ( - component: React.ReactElement, - initialEntries: string[] = ['/'] -) => { - const mockClient = createMockSupabaseClient(); - return render( - - - - {component} - - - - ); -}; - describe('AppHeader (Web)', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/shared-tests/__tests__/components/profile/AvatarUpload.native.test.tsx b/packages/shared-tests/__tests__/components/profile/AvatarUpload.native.test.tsx index 1e1e3aa6..a8675747 100644 --- a/packages/shared-tests/__tests__/components/profile/AvatarUpload.native.test.tsx +++ b/packages/shared-tests/__tests__/components/profile/AvatarUpload.native.test.tsx @@ -279,7 +279,7 @@ describe('AvatarUpload (Native)', () => { it('handles Android platform', async () => { const { Platform } = require('react-native'); Platform.OS = 'android'; - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = true; const { launchImageLibraryAsync } = require('expo-image-picker'); @@ -608,7 +608,7 @@ describe('AvatarUpload (Native)', () => { it('fixes URL for Android emulator in dev mode', () => { const { Platform } = require('react-native'); Platform.OS = 'android'; - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = true; render( @@ -626,7 +626,7 @@ describe('AvatarUpload (Native)', () => { expect(images.length).toBeGreaterThan(0); Platform.OS = 'ios'; - // @ts-ignore + // @ts-expect-error test-only assignment to global __DEV__ global.__DEV__ = false; }); diff --git a/packages/shared-tests/__tests__/components/profile/AvatarUpload.test.tsx b/packages/shared-tests/__tests__/components/profile/AvatarUpload.test.tsx index 02a94abc..0032e0cc 100644 --- a/packages/shared-tests/__tests__/components/profile/AvatarUpload.test.tsx +++ b/packages/shared-tests/__tests__/components/profile/AvatarUpload.test.tsx @@ -253,7 +253,7 @@ describe('AvatarUpload', () => { uploadedUrl: newAvatarUrl, // New uploaded URL }); - const { rerender } = render( + render( ({ })); // Mock AvatarUpload component -let mockOnUploadComplete: ((url: string) => Promise) | null = null; -let mockOnRemove: (() => Promise) | null = null; - jest.mock('@beakerstack/shared/components/profile/AvatarUpload.native', () => ({ AvatarUpload: ({ onUploadComplete, onRemove }: any) => { - mockOnUploadComplete = onUploadComplete; - mockOnRemove = onRemove; return (