code quality
Some checks failed
CI / lint-typecheck (pull_request) Failing after 13s
CI / tests (pull_request) Has been skipped
CI / contract-tests (pull_request) Has been skipped
CI / e2e-tests (pull_request) Has been skipped
CI / comment-pr (pull_request) Has been skipped
CI / commit-types (pull_request) Has been skipped

This commit is contained in:
2026-01-27 16:30:03 +01:00
parent 9b31eaf728
commit 9894c4a841
34 changed files with 926 additions and 536 deletions

View File

@@ -64,7 +64,7 @@ function getEnvironment(): string {
function validateEnvironment(
env: string
): env is keyof FeatureFlagConfig {
const validEnvs = ['development', 'test', 'staging', 'production'];
const validEnvs = ['development', 'test', 'e2e', 'staging', 'production'];
if (!validEnvs.includes(env)) {
throw new Error(
`Invalid environment: "${env}". Valid environments: ${validEnvs.join(', ')}`

View File

@@ -32,6 +32,7 @@ export interface EnvironmentConfig {
export interface FeatureFlagConfig {
development: EnvironmentConfig;
test: EnvironmentConfig;
e2e: EnvironmentConfig;
staging: EnvironmentConfig;
production: EnvironmentConfig;
}

View File

@@ -129,6 +129,43 @@ export const featureConfig: FeatureFlagConfig = {
},
},
// E2E environment - same as test
e2e: {
platform: {
dashboard: 'enabled',
leagues: 'enabled',
teams: 'enabled',
drivers: 'enabled',
races: 'enabled',
leaderboards: 'enabled',
},
auth: {
signup: 'enabled',
login: 'enabled',
forgotPassword: 'enabled',
resetPassword: 'enabled',
},
onboarding: {
wizard: 'enabled',
},
sponsors: {
portal: 'enabled',
dashboard: 'enabled',
management: 'enabled',
campaigns: 'enabled',
billing: 'enabled',
},
admin: {
dashboard: 'enabled',
userManagement: 'enabled',
analytics: 'enabled',
},
beta: {
newUI: 'disabled',
experimental: 'disabled',
},
},
// Staging environment - controlled feature rollout
staging: {
// Core platform features

View File

@@ -7,16 +7,17 @@ import React from 'react';
interface AuthFormProps {
children: React.ReactNode;
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
'data-testid'?: string;
}
/**
* AuthForm
*
*
* Semantic form wrapper for auth flows.
*/
export function AuthForm({ children, onSubmit }: AuthFormProps) {
export function AuthForm({ children, onSubmit, 'data-testid': testId }: AuthFormProps) {
return (
<Form onSubmit={onSubmit}>
<Form onSubmit={onSubmit} data-testid={testId}>
<Group direction="column" gap={6}>
{children}
</Group>

View File

@@ -8,25 +8,28 @@ interface KpiItem {
interface DashboardKpiRowProps {
items: KpiItem[];
'data-testid'?: string;
}
/**
* DashboardKpiRow
*
*
* A horizontal row of key performance indicators with telemetry styling.
*/
export function DashboardKpiRow({ items }: DashboardKpiRowProps) {
export function DashboardKpiRow({ items, 'data-testid': testId }: DashboardKpiRowProps) {
return (
<StatGrid
variant="card"
cardVariant="dark"
font="mono"
columns={{ base: 2, md: 3, lg: 6 }}
stats={items.map(item => ({
stats={items.map((item, index) => ({
label: item.label,
value: item.value,
intent: item.intent as any
intent: item.intent as any,
'data-testid': `stat-${item.label.toLowerCase()}`
}))}
data-testid={testId}
/>
);
}

View File

@@ -43,8 +43,8 @@ export function RecentActivityTable({ items }: RecentActivityTableProps) {
</TableHead>
<TableBody>
{items.map((item) => (
<TableRow key={item.id}>
<TableCell>
<TableRow key={item.id} data-testid={`activity-item-${item.id}`}>
<TableCell data-testid="activity-race-result-link">
<Text font="mono" variant="telemetry" size="xs">{item.type}</Text>
</TableCell>
<TableCell>

View File

@@ -5,16 +5,17 @@ import React from 'react';
interface TelemetryPanelProps {
title: string;
children: React.ReactNode;
'data-testid'?: string;
}
/**
* TelemetryPanel
*
*
* A dense, instrument-grade panel for displaying data and controls.
*/
export function TelemetryPanel({ title, children }: TelemetryPanelProps) {
export function TelemetryPanel({ title, children, 'data-testid': testId }: TelemetryPanelProps) {
return (
<Panel title={title} variant="dark" padding={4}>
<Panel title={title} variant="dark" padding={4} data-testid={testId}>
<Text size="sm" variant="med">
{children}
</Text>

View File

@@ -91,7 +91,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
};
return (
<Stack gap={8}>
<Stack gap={8} data-testid="avatar-creation-form">
{/* Photo Upload */}
<Stack>
<Text as="label" size="sm" weight="medium" color="text-gray-300" block mb={3}>
@@ -100,6 +100,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
<Stack direction="row" gap={6}>
{/* Upload Area */}
<Stack
data-testid="photo-upload-area"
onClick={() => fileInputRef.current?.click()}
flex={1}
display="flex"
@@ -126,6 +127,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
accept="image/*"
onChange={handleFileSelect}
display="none"
data-testid="photo-upload-input"
/>
{avatarInfo.isValidating ? (
@@ -144,6 +146,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
objectFit="cover"
fullWidth
fullHeight
data-testid="photo-preview"
/>
</Stack>
<Text size="sm" color="text-performance-green" block>
@@ -199,11 +202,12 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
Racing Suit Color
</Stack>
</Text>
<Stack flexDirection="row" flexWrap="wrap" gap={2}>
<Stack flexDirection="row" flexWrap="wrap" gap={2} data-testid="suit-color-options">
{SUIT_COLORS.map((color) => (
<Button
key={color.value}
type="button"
data-testid={`suit-color-${color.value}`}
onClick={() => setAvatarInfo({ ...avatarInfo, suitColor: color.value })}
rounded="lg"
transition
@@ -235,6 +239,7 @@ export function AvatarStep({ avatarInfo, setAvatarInfo, errors, setErrors, onGen
<Stack>
<Button
type="button"
data-testid="generate-avatars-btn"
variant="primary"
onClick={onGenerateAvatars}
disabled={avatarInfo.isGenerating || avatarInfo.isValidating}

View File

@@ -49,6 +49,7 @@ export function OnboardingPrimaryActions({
onClick={onNext}
disabled={isLoading || !canNext}
w="40"
data-testid={isLastStep ? 'complete-onboarding-btn' : 'next-btn'}
>
<Stack direction="row" align="center" gap={2}>
{isLoading ? 'Processing...' : isLastStep ? 'Complete Setup' : nextLabel}

View File

@@ -17,7 +17,7 @@ interface OnboardingShellProps {
*/
export function OnboardingShell({ children, header, footer, sidebar }: OnboardingShellProps) {
return (
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white">
<Box minHeight="100vh" bg="rgba(10,10,10,1)" color="white" data-testid="onboarding-wizard">
{header && (
<Box borderBottom borderColor="rgba(255,255,255,0.1)" py={4} bg="rgba(20,22,25,1)">
<Container size="md">

View File

@@ -15,8 +15,9 @@ interface OnboardingStepPanelProps {
* Provides a consistent header and surface.
*/
export function OnboardingStepPanel({ title, description, children }: OnboardingStepPanelProps) {
const testId = title.toLowerCase().includes('personal') ? 'step-1-personal-info' : 'step-2-avatar';
return (
<Stack gap={6}>
<Stack gap={6} data-testid={testId}>
<Stack gap={1}>
<Text as="h2" size="2xl" weight="bold" color="text-white" letterSpacing="tight">
{title}

View File

@@ -49,6 +49,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
</Text>
<Input
id="firstName"
data-testid="first-name-input"
type="text"
value={personalInfo.firstName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -67,6 +68,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
</Text>
<Input
id="lastName"
data-testid="last-name-input"
type="text"
value={personalInfo.lastName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -86,6 +88,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
</Text>
<Input
id="displayName"
data-testid="display-name-input"
type="text"
value={personalInfo.displayName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@@ -104,6 +107,7 @@ export function PersonalInfoStep({ personalInfo, setPersonalInfo, errors, loadin
Country *
</Text>
<CountrySelect
data-testid="country-select"
value={personalInfo.country}
onChange={(value: string) =>
setPersonalInfo({ ...personalInfo, country: value })

View File

@@ -57,34 +57,34 @@ export function DashboardTemplate({
return (
<Stack gap={6}>
{/* KPI Overview */}
<DashboardKpiRow items={kpiItems} />
<DashboardKpiRow items={kpiItems} data-testid="dashboard-stats" />
<Grid responsiveGridCols={{ base: 1, lg: 12 }} gap={6}>
{/* Main Content Column */}
<Box responsiveColSpan={{ base: 1, lg: 8 }}>
<Stack direction="col" gap={6}>
{nextRace && (
<TelemetryPanel title="Active Session">
<TelemetryPanel title="Active Session" data-testid="next-race-section">
<Box display="flex" alignItems="center" justifyContent="between">
<Box>
<Text size="xs" variant="low" mb={1} block>Next Event</Text>
<Text size="lg" weight="bold" block>{nextRace.track}</Text>
<Text size="xs" variant="primary" font="mono" block>{nextRace.car}</Text>
<Text size="lg" weight="bold" block data-testid="next-race-track">{nextRace.track}</Text>
<Text size="xs" variant="primary" font="mono" block data-testid="next-race-car">{nextRace.car}</Text>
</Box>
<Box textAlign="right">
<Text size="xs" variant="low" mb={1} block>Starts In</Text>
<Text size="xl" font="mono" weight="bold" variant="warning" block>{nextRace.timeUntil}</Text>
<Text size="xs" variant="low" mb={1} block data-testid="next-race-time">{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
<Text size="xl" font="mono" weight="bold" variant="warning" block data-testid="next-race-countdown">{nextRace.timeUntil}</Text>
<Text size="xs" variant="low" block>{nextRace.formattedDate} @ {nextRace.formattedTime}</Text>
</Box>
</Box>
</TelemetryPanel>
)}
<TelemetryPanel title="Recent Activity">
<TelemetryPanel title="Recent Activity" data-testid="activity-feed-section">
{hasFeedItems ? (
<RecentActivityTable items={activityItems} />
) : (
<Box py={8} textAlign="center">
<Box py={8} textAlign="center" data-testid="activity-empty">
<Text italic variant="low">No recent activity recorded.</Text>
</Box>
)}
@@ -95,12 +95,22 @@ export function DashboardTemplate({
{/* Sidebar Column */}
<Box responsiveColSpan={{ base: 1, lg: 4 }}>
<Stack direction="col" gap={6}>
<TelemetryPanel title="Championship Standings">
<TelemetryPanel title="Championship Standings" data-testid="championship-standings-section">
{hasLeagueStandings ? (
<Stack direction="col" gap={3}>
{leagueStandings.map((standing) => (
<Box key={standing.leagueId} display="flex" alignItems="center" justifyContent="between" borderBottom borderColor="var(--ui-color-border-muted)" pb={2}>
<Box>
<Box
key={standing.leagueId}
display="flex"
alignItems="center"
justifyContent="between"
borderBottom
borderColor="var(--ui-color-border-muted)"
pb={2}
data-testid={`league-standing-${standing.leagueId}`}
cursor="pointer"
>
<Box data-testid="league-standing-link">
<Text size="xs" weight="bold" truncate block maxWidth="180px">{standing.leagueName}</Text>
<Text size="xs" variant="low" block>Pos: {standing.position} / {standing.totalDrivers}</Text>
</Box>
@@ -109,30 +119,37 @@ export function DashboardTemplate({
))}
</Stack>
) : (
<Box py={4} textAlign="center">
<Box py={4} textAlign="center" data-testid="standings-empty">
<Text italic variant="low">No active championships.</Text>
</Box>
)}
</TelemetryPanel>
<TelemetryPanel title="Upcoming Schedule">
<TelemetryPanel title="Upcoming Schedule" data-testid="upcoming-races-section">
<Stack direction="col" gap={4}>
{upcomingRaces.slice(0, 3).map((race) => (
<Box key={race.id} cursor="pointer">
<Box display="flex" justifyContent="between" alignItems="start" mb={1}>
<Text size="xs" weight="bold">{race.track}</Text>
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="xs" variant="low">{race.car}</Text>
<Text size="xs" variant="low">{race.formattedDate}</Text>
{upcomingRaces.length > 0 ? (
upcomingRaces.slice(0, 3).map((race) => (
<Box key={race.id} cursor="pointer" data-testid={`upcoming-race-${race.id}`}>
<Box display="flex" justifyContent="between" alignItems="start" mb={1} data-testid="upcoming-race-link">
<Text size="xs" weight="bold">{race.track}</Text>
<Text size="xs" font="mono" variant="low">{race.timeUntil}</Text>
</Box>
<Box display="flex" justifyContent="between">
<Text size="xs" variant="low">{race.car}</Text>
<Text size="xs" variant="low">{race.formattedDate}</Text>
</Box>
</Box>
))
) : (
<Box py={4} textAlign="center" data-testid="upcoming-races-empty">
<Text italic variant="low">No upcoming races.</Text>
</Box>
))}
<Button
variant="secondary"
fullWidth
)}
<Button
variant="secondary"
fullWidth
onClick={onNavigateToRaces}
data-testid="view-full-schedule-link"
>
<Text size="xs" weight="bold" uppercase letterSpacing="widest">View Full Schedule</Text>
</Button>

View File

@@ -45,8 +45,8 @@ export function RacesIndexTemplate({
const hasRaces = viewData.racesByDate.length > 0;
return (
<Section variant="default" padding="md">
<PageHeader
<Section variant="default" padding="md" data-testid="races-list">
<PageHeader
title="Races"
description="Live Sessions & Upcoming Events"
icon={Flag}

View File

@@ -42,7 +42,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
title="Welcome Back"
description="Sign in to access your racing dashboard"
>
<AuthForm onSubmit={formActions.handleSubmit}>
<AuthForm onSubmit={formActions.handleSubmit} data-testid="login-form">
<Group direction="column" gap={4} fullWidth>
<Input
label="Email Address"
@@ -56,6 +56,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
disabled={isSubmitting}
autoComplete="email"
icon={<Mail size={16} />}
data-testid="email-input"
/>
<Group direction="column" gap={1.5} fullWidth>
@@ -71,6 +72,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
autoComplete="current-password"
showPassword={viewData.showPassword}
onTogglePassword={() => formActions.setShowPassword(!viewData.showPassword)}
data-testid="password-input"
/>
<Group justify="end" fullWidth>
<Link href={routes.auth.forgotPassword}>
@@ -127,6 +129,7 @@ export function LoginTemplate({ viewData, formActions, mutationState }: LoginTem
disabled={isSubmitting}
fullWidth
icon={isSubmitting ? <LoadingSpinner size={4} /> : <LogIn size={16} />}
data-testid="login-submit"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>

View File

@@ -91,8 +91,9 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
borderWidth,
aspectRatio,
border,
...props
}, ref) => {
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold transition-all duration-150 ease-in-out';
const baseClasses = 'inline-flex items-center justify-center focus-visible:outline focus-visible:outline-1 focus-visible:outline-offset-2 active:opacity-80 uppercase tracking-widest font-bold transition-all duration-150 ease-in-out';
const variantClasses = {
primary: 'bg-[var(--ui-color-intent-primary)] text-white hover:opacity-90 focus-visible:outline-[var(--ui-color-intent-primary)] shadow-[0_0_15px_rgba(25,140,255,0.2)]',
@@ -154,6 +155,8 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
const Tag = as === 'a' ? 'a' : 'button';
const { 'data-testid': testId } = (props as any) || {};
return (
<Tag
ref={ref as any}
@@ -166,6 +169,7 @@ export const Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonPr
disabled={as === 'a' ? undefined : (disabled || isLoading)}
style={combinedStyle}
title={title}
data-testid={testId}
>
{content}
</Tag>

View File

@@ -93,6 +93,7 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(({
gap,
borderLeft,
justifyContent,
...props
}, ref) => {
const variantClasses = {
default: 'bg-[var(--ui-color-bg-surface)] border-[var(--ui-color-border-default)] shadow-sm',
@@ -152,11 +153,12 @@ export const Card = forwardRef<HTMLDivElement, CardProps>(({
};
return (
<div
<div
ref={ref}
className={classes}
onClick={onClick}
style={Object.keys(style).length > 0 ? style : undefined}
{...props}
>
{title && (
<div className={`border-b border-[var(--ui-color-border-muted)] ${typeof padding === 'string' ? (paddingClasses[padding] || paddingClasses.md) : ''}`}>

View File

@@ -5,21 +5,24 @@ export interface FormProps {
onSubmit?: FormEventHandler<HTMLFormElement>;
noValidate?: boolean;
className?: string;
'data-testid'?: string;
}
export const Form = forwardRef<HTMLFormElement, FormProps>(({
children,
onSubmit,
export const Form = forwardRef<HTMLFormElement, FormProps>(({
children,
onSubmit,
noValidate = true,
className
className,
'data-testid': testId
}, ref) => {
return (
<form
<form
ref={ref}
onSubmit={onSubmit}
onSubmit={onSubmit}
noValidate={noValidate}
className={className}
style={{ width: '100%' }}
data-testid={testId}
>
{children}
</form>

View File

@@ -28,8 +28,9 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
hint,
id,
size,
...props
...props
}, ref) => {
const { 'data-testid': testId, ...restProps } = props as any;
const variantClasses = {
default: 'bg-surface-charcoal border border-outline-steel focus:border-primary-accent',
ghost: 'bg-transparent border-none',
@@ -80,7 +81,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(({
ref={ref}
id={inputId}
className="bg-transparent border-none outline-none text-sm w-full text-text-high placeholder:text-text-low/50 h-full"
{...props}
data-testid={testId}
{...restProps}
/>
{rightElement}

View File

@@ -18,9 +18,9 @@ export interface PanelProps {
bg?: string;
}
export function Panel({
children,
variant = 'default',
export function Panel({
children,
variant = 'default',
padding = 'md',
onClick,
style,
@@ -30,8 +30,9 @@ export function Panel({
footer,
border,
rounded,
className
}: PanelProps) {
className,
...props
}: PanelProps & { [key: string]: any }) {
const variantClasses = {
default: 'bg-[var(--ui-color-bg-surface)] border border-[var(--ui-color-border-default)] shadow-sm',
muted: 'bg-[var(--ui-color-bg-surface-muted)] border border-[var(--ui-color-border-muted)]',
@@ -61,13 +62,14 @@ export function Panel({
: '';
return (
<div
<div
className={`${variantClasses[variant]} ${getPaddingClass(padding)} ${interactiveClasses} ${rounded ? `rounded-${rounded}` : 'rounded-md'} ${border ? 'border' : ''} ${className || ''}`}
onClick={onClick}
style={{
...style,
...(typeof padding === 'number' ? { padding: `${padding * 0.25}rem` } : {})
}}
{...props}
>
{(title || actions) && (
<div className="flex items-center justify-between mb-6 border-b border-[var(--ui-color-border-muted)] pb-4">

View File

@@ -22,10 +22,10 @@ export interface StatCardProps {
delay?: number;
}
export const StatCard = ({
label,
value,
icon,
export const StatCard = ({
label,
value,
icon,
intent: intentProp,
variant = 'default',
font = 'sans',
@@ -33,8 +33,9 @@ export const StatCard = ({
footer,
suffix,
prefix,
delay
}: StatCardProps) => {
delay,
...props
}: StatCardProps & { [key: string]: any }) => {
const variantMap: Record<string, { variant: any, intent: any }> = {
blue: { variant: 'default', intent: 'primary' },
green: { variant: 'default', intent: 'success' },
@@ -46,7 +47,7 @@ export const StatCard = ({
const finalIntent = mapped.intent;
return (
<Card variant={finalVariant}>
<Card variant={finalVariant} {...props}>
<Box display="flex" alignItems="start" justifyContent="between" marginBottom={4}>
<Box>
<Text size="xs" weight="bold" variant="low" uppercase>

View File

@@ -11,24 +11,25 @@ export interface StatGridProps {
font?: 'sans' | 'mono';
}
export const StatGrid = ({
stats,
export const StatGrid = ({
stats,
columns = 3,
variant = 'box',
cardVariant,
font
}: StatGridProps) => {
font,
...props
}: StatGridProps & { [key: string]: any }) => {
return (
<Grid cols={columns} gap={4}>
<Grid cols={columns} gap={4} {...props}>
{stats.map((stat, index) => (
variant === 'box' ? (
<StatBox key={index} {...(stat as StatBoxProps)} />
) : (
<StatCard
key={index}
variant={cardVariant}
font={font}
{...(stat as StatCardProps)}
<StatCard
key={index}
variant={cardVariant}
font={font}
{...(stat as StatCardProps)}
/>
)
))}