feat: add ESLint configuration and testing infrastructure

Add comprehensive ESLint setup with React and TypeScript support, create basic integration tests for the shutdown utilities, and enhance the Justfile with a new check command that runs all validation steps (cargo check, clippy, tests, and linting).
This commit is contained in:
Ryan Walters
2025-11-03 02:21:35 -06:00
parent 47c23459f1
commit b1ed2434f8
9 changed files with 2248 additions and 6 deletions

View File

@@ -1,5 +1,14 @@
default_services := "bot,web,scraper"
default:
just --list
check:
cargo check
cargo clippy
cargo nextest run
pnpm run -C web lint
# Auto-reloading frontend server
frontend:
pnpm run -C web dev

33
tests/basic_test.rs Normal file
View File

@@ -0,0 +1,33 @@
use banner::utils::shutdown::join_tasks;
use tokio::task::JoinHandle;
#[tokio::test]
async fn test_join_tasks_success() {
// Create some tasks that complete successfully
let handles: Vec<JoinHandle<()>> = vec![
tokio::spawn(async { tokio::time::sleep(tokio::time::Duration::from_millis(10)).await }),
tokio::spawn(async { tokio::time::sleep(tokio::time::Duration::from_millis(20)).await }),
tokio::spawn(async { /* immediate completion */ }),
];
// All tasks should complete successfully
let result = join_tasks(handles).await;
assert!(result.is_ok(), "Expected all tasks to complete successfully");
}
#[tokio::test]
async fn test_join_tasks_with_panic() {
// Create some tasks, including one that panics
let handles: Vec<JoinHandle<()>> = vec![
tokio::spawn(async { tokio::time::sleep(tokio::time::Duration::from_millis(10)).await }),
tokio::spawn(async { panic!("intentional test panic") }),
tokio::spawn(async { /* immediate completion */ }),
];
// Should return an error because one task panicked
let result = join_tasks(handles).await;
assert!(result.is_err(), "Expected an error when a task panics");
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("1 task(s) panicked"), "Error message should mention panicked tasks");
}

63
web/eslint.config.js Normal file
View File

@@ -0,0 +1,63 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
export default tseslint.config(
// Ignore generated files and build outputs
{
ignores: ['dist', 'node_modules', 'src/routeTree.gen.ts', '*.config.js'],
},
// Base configs
js.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
// React plugin configuration
{
files: ['**/*.{ts,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
react: {
version: '19.0',
},
},
rules: {
// React rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
...reactHooks.configs.recommended.rules,
// React Refresh
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// TypeScript overrides
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-explicit-any': 'warn',
// Disable prop-types since we're using TypeScript
'react/prop-types': 'off',
},
}
);

View File

@@ -7,7 +7,8 @@
"start": "vite --port 3000",
"build": "vite build && tsc",
"serve": "vite preview",
"test": "vitest run"
"test": "vitest run",
"lint": "tsc && eslint . --ext .ts,.tsx"
},
"dependencies": {
"@radix-ui/themes": "^3.2.1",
@@ -23,14 +24,20 @@
"recharts": "^3.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^24.3.3",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.39.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"jsdom": "^26.0.0",
"typescript": "^5.7.2",
"typescript-eslint": "^8.46.2",
"vite": "^6.3.5",
"vitest": "^3.0.5",
"web-vitals": "^4.2.4"

2129
web/pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ export class BannerApiClient {
);
}
return response.json();
return (await response.json()) as T;
}
async getHealth(): Promise<HealthResponse> {

View File

@@ -1,6 +1,6 @@
const reportWebVitals = (onPerfEntry?: () => void) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
void import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => {
onCLS(onPerfEntry)
onINP(onPerfEntry)
onFCP(onPerfEntry)

View File

@@ -237,11 +237,11 @@ function App() {
}
// Schedule the next request after the current one completes
timeoutId = setTimeout(fetchData, REFRESH_INTERVAL);
timeoutId = setTimeout(() => void fetchData(), REFRESH_INTERVAL);
};
// Start the first request immediately
fetchData();
void fetchData();
return () => {
if (timeoutId) {

View File

@@ -11,6 +11,7 @@
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */