diff --git a/eslint.config.mjs b/eslint.config.mjs index aa94178c..481e88af 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -61,10 +61,15 @@ export default tseslint.config( '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unsafe-return': 'off', '@typescript-eslint/no-unsafe-enum-comparison': 'off', - '@typescript-eslint/no-unnecessary-type-assertion': 'off', - '@typescript-eslint/no-redundant-type-constituents': 'off', + }, + }, + { + // Test files: `async` test callbacks and `act(async () => {})` wrappers are + // idiomatic even when they contain no `await` (async `act` flushes the + // microtask queue). Relaxing require-await here avoids churn in test infra. + files: ['**/*.spec.{ts,tsx}', '**/*.test.{ts,tsx}'], + rules: { '@typescript-eslint/require-await': 'off', - '@typescript-eslint/no-misused-promises': 'off', }, }, prettierRecommended, diff --git a/src/app/[locale]/complete-registration/CompleteRegistration.tsx b/src/app/[locale]/complete-registration/CompleteRegistration.tsx index 2d84892d..e2615cd2 100644 --- a/src/app/[locale]/complete-registration/CompleteRegistration.tsx +++ b/src/app/[locale]/complete-registration/CompleteRegistration.tsx @@ -86,7 +86,7 @@ export default function CompleteRegistration(): React.ReactElement { validationSchema: CompleteRegistrationSchema, validateOnChange: isSubmitted, validateOnBlur: true, - onSubmit: async (values) => { + onSubmit: (values) => { if (user != null) { dispatch( refreshUserInformation({ diff --git a/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx b/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx index a203f336..35893604 100644 --- a/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx +++ b/src/app/[locale]/contribute/FeedSubmission/Form/FirstStep.tsx @@ -107,7 +107,11 @@ export default function FormFirstStep({ return ( <> -
+ { + void handleSubmit(onSubmit)(e); + }} + > - + { + void handleSubmit(onSubmit)(e); + }} + > diff --git a/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx b/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx index 76fccaa4..2e5ba37f 100644 --- a/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx +++ b/src/app/[locale]/contribute/FeedSubmission/Form/SecondStep.tsx @@ -59,7 +59,11 @@ export default function FormSecondStep({ return ( <> {t('gtfsScheduleFeed')} - + { + void handleSubmit(onSubmit)(e); + }} + > {t('gtfsRealtimeFeed')} - + { + void handleSubmit(onSubmit)(e); + }} + > - + { + void handleSubmit(onSubmit)(e); + }} + > {/* Show required emptyLicenseUsage if official producer and no license provided */} {isOfficialProducer && noLicenseProvided && ( diff --git a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx index 2beedda2..7d92a7dd 100644 --- a/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx +++ b/src/app/[locale]/feeds/[feedDataType]/[feedId]/static/layout.tsx @@ -24,9 +24,9 @@ interface Props { * from the clean URLs like /feeds/gtfs/mdb-123. * TODO: Now that legacy catch-all route is removed, change this to a private route `_static` and update proxy and links accordingly. */ -export default async function StaticFeedLayout({ +export default function StaticFeedLayout({ children, params, -}: Props): Promise { +}: Props): React.ReactElement { return <>{children}; } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 03610522..93b769e0 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -84,7 +84,7 @@ export default async function LocaleLayout({ } // At this point, locale is guaranteed to be a valid Locale type - const validLocale = locale as Locale; + const validLocale = locale; // Enable static rendering for this locale setRequestLocale(validLocale); diff --git a/src/app/api/revalidate/route.spec.ts b/src/app/api/revalidate/route.spec.ts index 47a1cd6d..38e60b37 100644 --- a/src/app/api/revalidate/route.spec.ts +++ b/src/app/api/revalidate/route.spec.ts @@ -43,7 +43,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(500); @@ -65,7 +65,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(401); @@ -87,7 +87,7 @@ describe('GET /api/revalidate', () => { }, }); - const response = await GET(request); + const response = GET(request); const json = await response.json(); expect(response.status).toBe(200); diff --git a/src/app/api/revalidate/route.ts b/src/app/api/revalidate/route.ts index 1ffa841d..b004953e 100644 --- a/src/app/api/revalidate/route.ts +++ b/src/app/api/revalidate/route.ts @@ -36,7 +36,7 @@ const defaultRevalidateOptions: RevalidateBody = { * Vercel automatically passes Authorization: Bearer with each invocation. * Configured in vercel.json under "crons" for 4am UTC Monday-Saturday and 7am UTC Sunday. */ -export async function GET(req: Request): Promise { +export function GET(req: Request): NextResponse { const authHeader = req.headers.get('authorization'); const cronSecret = process.env.CRON_SECRET; diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts index 5869b609..ccc12a0c 100644 --- a/src/app/api/session/route.ts +++ b/src/app/api/session/route.ts @@ -62,7 +62,7 @@ export async function POST(req: NextRequest): Promise { } } -export async function GET(req: NextRequest): Promise { +export function GET(req: NextRequest): NextResponse { try { const cookie = req.cookies.get(COOKIE_NAME)?.value; if (cookie == null) { @@ -89,6 +89,7 @@ export async function GET(req: NextRequest): Promise { } export async function DELETE(req: NextRequest): Promise { + // Clear the session cookie so that subsequent requests have no session. const response = NextResponse.json({ status: 'logged_out' }); response.cookies.delete(COOKIE_NAME); return response; diff --git a/src/app/components/GtfsVisualizationMap.spec.tsx b/src/app/components/GtfsVisualizationMap.spec.tsx index 4e61e0b8..db030ed2 100644 --- a/src/app/components/GtfsVisualizationMap.spec.tsx +++ b/src/app/components/GtfsVisualizationMap.spec.tsx @@ -94,7 +94,7 @@ describe('generateStopColorExpression', () => { describe('generateRouteOutlineColorExpression', () => { // Helper to safely index into nested unknown arrays - type NestedArr = Array; + type NestedArr = unknown[]; const at = (arr: NestedArr, ...indices: number[]): NestedArr => indices.reduce((a, i) => (a as NestedArr[])[i], arr); it('returns an array expression starting with "let" and "rgba"', () => { diff --git a/src/app/components/Map/SelectedRoutesStopsPanel.tsx b/src/app/components/Map/SelectedRoutesStopsPanel.tsx index 4a259624..bbfe374c 100644 --- a/src/app/components/Map/SelectedRoutesStopsPanel.tsx +++ b/src/app/components/Map/SelectedRoutesStopsPanel.tsx @@ -79,7 +79,7 @@ export const SelectedRoutesStopsPanel = ( key={s.stopId} role='button' tabIndex={0} - aria-selected={isActive ? 'true' : 'false'} + aria-pressed={isActive ? 'true' : 'false'} onClick={() => { focusStopFromPanel(s); }} diff --git a/src/app/components/ThemeToggle.tsx b/src/app/components/ThemeToggle.tsx index b1ec9cd4..24e246de 100644 --- a/src/app/components/ThemeToggle.tsx +++ b/src/app/components/ThemeToggle.tsx @@ -2,13 +2,14 @@ import { IconButton } from '@mui/material'; import Brightness4Icon from '@mui/icons-material/Brightness4'; import Brightness7Icon from '@mui/icons-material/Brightness7'; import { useTheme } from '../context/ThemeProvider'; +import { ThemeModeEnum } from '../Theme'; const ThemeToggle = (): React.ReactElement => { const { mode, toggleTheme } = useTheme(); return ( - {mode === 'dark' ? : } + {mode === ThemeModeEnum.dark ? : } ); }; diff --git a/src/app/screens/Feed/components/NotificationSettingsDialog.tsx b/src/app/screens/Feed/components/NotificationSettingsDialog.tsx index 68fe6a0b..dd219b99 100644 --- a/src/app/screens/Feed/components/NotificationSettingsDialog.tsx +++ b/src/app/screens/Feed/components/NotificationSettingsDialog.tsx @@ -149,6 +149,7 @@ export default function NotificationSettingsDialog({ )} + {/* Type of changes */} diff --git a/src/app/screens/FeedSubmitted.tsx b/src/app/screens/FeedSubmitted.tsx index 6cfa7427..8e265c63 100644 --- a/src/app/screens/FeedSubmitted.tsx +++ b/src/app/screens/FeedSubmitted.tsx @@ -1,6 +1,7 @@ 'use client'; import { Typography, Box, Container, useTheme, Button } from '@mui/material'; +import Image from 'next/image'; export default function FeedSubmitted(): React.ReactElement { const theme = useTheme(); @@ -21,10 +22,13 @@ export default function FeedSubmitted(): React.ReactElement { 🚀 Your feed has been submitted! - rocket diff --git a/src/app/screens/GbfsValidator/GbfsFeedSearchInput.tsx b/src/app/screens/GbfsValidator/GbfsFeedSearchInput.tsx index 3fa621d3..cb821691 100644 --- a/src/app/screens/GbfsValidator/GbfsFeedSearchInput.tsx +++ b/src/app/screens/GbfsValidator/GbfsFeedSearchInput.tsx @@ -37,7 +37,7 @@ export default function GbfsFeedSearchInput({ initialFeedUrl ?? '', ); const [requiresAuth, setRequiresAuth] = useState(false); - const [authType, setAuthType] = useState(''); + const [authType, setAuthType] = useState(''); const [basicAuthUsername, setBasicAuthUsername] = useState< string | undefined >(undefined); @@ -65,7 +65,9 @@ export default function GbfsFeedSearchInput({ // Used to keep the auth inputs up to date useEffect(() => { setRequiresAuth(auth !== undefined); - setAuthType(auth == undefined ? '' : (auth.authType ?? '')); + setAuthType( + auth == undefined ? '' : ((auth.authType as AuthTypeEnum) ?? ''), + ); setBasicAuthUsername( auth != null && 'username' in auth ? auth.username : undefined, ); @@ -91,7 +93,7 @@ export default function GbfsFeedSearchInput({ }; const handleAuthTypeChange = (event: SelectChangeEvent): void => { - setAuthType(event.target.value); + setAuthType(event.target.value as AuthTypeEnum); setBasicAuthUsername(undefined); setBasicAuthPassword(undefined); setBearerAuthValue(undefined); diff --git a/src/app/screens/GbfsValidator/components/ErrorDetailsDialog.tsx b/src/app/screens/GbfsValidator/components/ErrorDetailsDialog.tsx index e045892e..de866a11 100644 --- a/src/app/screens/GbfsValidator/components/ErrorDetailsDialog.tsx +++ b/src/app/screens/GbfsValidator/components/ErrorDetailsDialog.tsx @@ -431,7 +431,7 @@ export function ErrorDetailsDialog({ )} {renderHighlightedObject( - parentContextData as JSONValue, + parentContextData, null, null, )} @@ -439,7 +439,7 @@ export function ErrorDetailsDialog({ ); } return renderHighlightedObject( - parentContextData as JSONValue, + parentContextData, lastPointerSegment ?? null, lastArrayIndex, ); diff --git a/src/app/services/api-auth-middleware.ts b/src/app/services/api-auth-middleware.ts index d83ba77b..1f30c393 100644 --- a/src/app/services/api-auth-middleware.ts +++ b/src/app/services/api-auth-middleware.ts @@ -25,7 +25,7 @@ export const generateAuthMiddlewareWithToken = ( userContextJwt?: string, ): Middleware => { return { - async onRequest(req) { + onRequest(req) { // Always attach the bearer token for IAP/GCIP. req.headers.set('Authorization', `Bearer ${accessToken}`); diff --git a/src/app/services/profile-service.ts b/src/app/services/profile-service.ts index ef6158fc..02503abc 100644 --- a/src/app/services/profile-service.ts +++ b/src/app/services/profile-service.ts @@ -22,7 +22,7 @@ export const sendEmailVerification = async (): Promise => { /** * Return the current user or null if the user is not logged in. */ -export const getUserFromSession = async (): Promise => { +export const getUserFromSession = (): User | null => { const currentUser = app.auth().currentUser; if (currentUser === null) { return null; diff --git a/src/app/types.ts b/src/app/types.ts index 43898e54..9da1d6cb 100644 --- a/src/app/types.ts +++ b/src/app/types.ts @@ -80,19 +80,19 @@ export enum LicenseErrorSource { } export interface ProfileError { - code: string | 'unknown'; + code: string; message: string; source?: ProfileErrorSource; } export interface FeedError { - code: string | 'unknown'; + code: string; message: string; source?: FeedErrorSource; } export interface LicenseError { - code: string | 'unknown'; + code: string; message: string; source?: LicenseErrorSource; } diff --git a/src/app/utils/precompute.ts b/src/app/utils/precompute.ts index 62bce45e..83490aad 100644 --- a/src/app/utils/precompute.ts +++ b/src/app/utils/precompute.ts @@ -42,13 +42,7 @@ export interface PrecomputeDeps { /** Small helper: wait once for a map event */ async function once(map: maplibregl.Map, ev: string): Promise { - await new Promise( - // eslint-disable-next-line no-async-promise-executor - async (resolve) => - await map.once(ev, () => { - resolve(); - }), - ); + await map.once(ev); } // Extend helpers for [minLng,minLat,maxLng,maxLat]