Repository navigation
RD-T49 Login Fixes #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RD-T49 Login Fixes #155
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { act, fireEvent, render, screen } from '@testing-library/react-native'; | ||
| import React from 'react'; | ||
|
|
||
| import { useServerUrlStore } from '@/stores/app/server-url-store'; | ||
|
|
||
| import { LoginServerBadge } from '../login-server-badge'; | ||
|
|
||
| jest.mock('react-i18next', () => ({ | ||
| useTranslation: () => ({ | ||
| t: (key: string, options?: Record<string, string>) => | ||
| ({ | ||
| 'login.server.label': 'Server:', | ||
| 'login.server.us_west': 'US West', | ||
| 'login.server.eu_central': 'EU Central', | ||
| })[key] ?? (key === 'login.server.custom_host' ? `Custom (${options?.host})` : key), | ||
| }), | ||
| })); | ||
|
|
||
| jest.mock('nativewind', () => ({ useColorScheme: () => ({ colorScheme: 'light' }) })); | ||
| jest.mock('@/api/config', () => ({ getSystemConfig: jest.fn() })); | ||
| jest.mock('@/lib/mapbox-token', () => ({ clearMapboxToken: jest.fn() })); | ||
| jest.mock('@/lib/storage/app', () => ({ | ||
| getBaseApiUrl: jest.fn(() => 'https://api.resgrid.com/api/v4'), | ||
| setBaseApiUrl: jest.fn(), | ||
| })); | ||
|
|
||
| describe('LoginServerBadge', () => { | ||
| beforeEach(() => { | ||
| useServerUrlStore.setState({ url: '' }); | ||
| }); | ||
|
|
||
| it('names the stored server before anything changes it', () => { | ||
| render(<LoginServerBadge />); | ||
| expect(screen.getByText('US West')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('follows a server change', async () => { | ||
| render(<LoginServerBadge />); | ||
| await act(async () => { | ||
| await useServerUrlStore.getState().setUrl('https://api-eu-central.resgrid.com/api/v4'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled rejection from the asynchronous Kody rule violation: Handle async operations with proper error handling try {
await useServerUrlStore.getState().setUrl('https://api-eu-central.resgrid.com/api/v4');
} catch (error) {
// Assert or report the failure appropriately.
}Prompt for LLMTalk to Kody by mentioning @kody Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction. |
||
| }); | ||
| expect(screen.getByText('EU Central')).toBeTruthy(); | ||
|
|
||
| await act(async () => { | ||
| await useServerUrlStore.getState().setUrl('https://rg.example.org/api/v4'); | ||
| }); | ||
| expect(screen.getByText('Custom (rg.example.org)')).toBeTruthy(); | ||
| }); | ||
|
|
||
| it('opens the server selector when pressed', () => { | ||
| const onPress = jest.fn(); | ||
| render(<LoginServerBadge onPress={onPress} />); | ||
| fireEvent.press(screen.getByTestId('login-server-badge')); | ||
| expect(onPress).toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('is a button only when it opens something', () => { | ||
| const { rerender } = render(<LoginServerBadge onPress={jest.fn()} />); | ||
| expect(screen.getByTestId('login-server-badge').props.accessibilityRole).toBe('button'); | ||
|
|
||
| rerender(<LoginServerBadge />); | ||
| expect(screen.getByTestId('login-server-badge').props.accessibilityRole).toBeUndefined(); | ||
| expect(screen.queryByRole('button')).toBeNull(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { Server } from 'lucide-react-native'; | ||
| import { useColorScheme } from 'nativewind'; | ||
| import React from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
| import { Pressable, View } from 'react-native'; | ||
|
|
||
| import { Text } from '@/components/ui/text'; | ||
| import { useLoginServer } from '@/hooks/use-login-server'; | ||
|
|
||
| interface LoginServerBadgeProps { | ||
| /** Opens the server selector; without it the badge is a plain label. */ | ||
| onPress?: () => void; | ||
| } | ||
|
|
||
| const BADGE_CLASS = 'flex-row items-center self-center rounded-full border border-gray-300 px-3 py-1.5 dark:border-gray-700'; | ||
|
|
||
| /** Names the server the login form signs in to (US West, EU Central or Custom), so a wrong server is spotted before signing in. */ | ||
| export const LoginServerBadge = ({ onPress }: LoginServerBadgeProps) => { | ||
| const { t } = useTranslation(); | ||
| const { colorScheme } = useColorScheme(); | ||
| const { name } = useLoginServer(); | ||
| const label = t('login.server.signing_in_to', { server: name }); | ||
|
|
||
| const content = ( | ||
| <> | ||
| <Server size={14} color={colorScheme === 'dark' ? '#9ca3af' : '#6b7280'} /> | ||
| <Text className="ml-1.5 text-sm text-gray-600 dark:text-gray-300"> | ||
| {t('login.server.label')} <Text className="text-sm font-semibold text-gray-900 dark:text-white">{name}</Text> | ||
| </Text> | ||
| </> | ||
| ); | ||
|
|
||
| // Only a badge that does something is a button; a label stays a label for assistive technologies. | ||
| if (!onPress) { | ||
| return ( | ||
| <View accessible accessibilityLabel={label} testID="login-server-badge" className={BADGE_CLASS}> | ||
| {content} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <Pressable onPress={onPress} accessibilityRole="button" accessibilityLabel={label} accessibilityHint={t('login.server.change_hint')} testID="login-server-badge" className={BADGE_CLASS}> | ||
| {content} | ||
| </Pressable> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { useMemo } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { describeLoginServer, type LoginServer } from '@/lib/login-server'; | ||
| import { getBaseApiUrl } from '@/lib/storage/app'; | ||
| import { useServerUrlStore } from '@/stores/app/server-url-store'; | ||
|
|
||
| /** | ||
| * The server this device signs in to, and its display name ("US West", "EU Central" or "Custom (host)"). Follows a | ||
| * server change made from the login screen, since saving one updates the server URL store. | ||
| */ | ||
| export const useLoginServer = (): LoginServer & { name: string } => { | ||
| const { t } = useTranslation(); | ||
| const storeUrl = useServerUrlStore((state) => state.url); | ||
| const apiUrl = storeUrl || getBaseApiUrl(); | ||
|
|
||
| return useMemo(() => { | ||
| const server = describeLoginServer(apiUrl); | ||
| const name = server.kind === 'custom' ? t('login.server.custom_host', { host: server.host }) : t(`login.server.${server.kind}`); | ||
| return { ...server, name }; | ||
| }, [apiUrl, t]); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { describeLoginServer } from '../login-server'; | ||
|
|
||
| jest.mock('@/api/config', () => ({ getSystemConfig: jest.fn() })); | ||
|
|
||
| describe('describeLoginServer', () => { | ||
| it('names the Resgrid hosted servers with or without the API suffix', () => { | ||
| expect(describeLoginServer('https://api.resgrid.com/api/v4')).toEqual({ kind: 'us_west', host: 'api.resgrid.com' }); | ||
| expect(describeLoginServer('https://API.resgrid.com/')).toEqual({ kind: 'us_west', host: 'api.resgrid.com' }); | ||
| expect(describeLoginServer('https://api-eu-central.resgrid.com/api/v4')).toEqual({ kind: 'eu_central', host: 'api-eu-central.resgrid.com' }); | ||
| }); | ||
|
|
||
| it('labels any other server custom, with its host', () => { | ||
| expect(describeLoginServer('https://qaapi.resgrid.dev/api/v4')).toEqual({ kind: 'custom', host: 'qaapi.resgrid.dev' }); | ||
| expect(describeLoginServer('http://localhost:5000/api/v4')).toEqual({ kind: 'custom', host: 'localhost:5000' }); | ||
| expect(describeLoginServer('https://resgrid.example.org/rg/api/v4')).toEqual({ kind: 'custom', host: 'resgrid.example.org' }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inline arrow function in the
LoginServerBadgeJSXonPressprop creates a new function on every render, degrading performance. Move the handler definition outside the render method.Kody rule violation: Avoid using .bind() or arrow functions in JSX props
Prompt for LLM
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.