diff --git a/.github/workflows/frontend-test.yml b/.github/workflows/frontend-test.yml index 0ff65c7..d72fa8d 100644 --- a/.github/workflows/frontend-test.yml +++ b/.github/workflows/frontend-test.yml @@ -18,61 +18,44 @@ jobs: strategy: matrix: - node-version: [20] + node-version: [ 20 ] steps: - name: Checkout code uses: actions/checkout@v4 - + - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 9 - + - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'pnpm' cache-dependency-path: 'frontend/pnpm-lock.yaml' - + - name: Install dependencies working-directory: ./frontend run: pnpm install --frozen-lockfile - + - name: Run type checking working-directory: ./frontend run: pnpm check - + - name: Run linting working-directory: ./frontend run: pnpm lint - + - name: Run tests working-directory: ./frontend run: pnpm test:run - + - name: Run tests with coverage working-directory: ./frontend run: pnpm test:coverage - - - name: Upload coverage reports - if: matrix.node-version == 20 - uses: codecov/codecov-action@v3 - with: - directory: ./frontend/coverage - flags: frontend - name: frontend-coverage - fail_ci_if_error: false - + - name: Build application working-directory: ./frontend - run: pnpm build - - - name: Upload build artifacts - if: matrix.node-version == 20 - uses: actions/upload-artifact@v4 - with: - name: frontend-build - path: frontend/build/ - retention-days: 7 + run: pnpm build \ No newline at end of file diff --git a/frontend/.prettierignore b/frontend/.prettierignore index 6562bcb..a49eb02 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -4,3 +4,7 @@ pnpm-lock.yaml yarn.lock bun.lock bun.lockb +static +node_modules +.svelte-kit +coverage \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index 33ec8ca..5693fea 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -20,4 +20,3 @@ pnpm run build ``` You can preview the production build with `pnpm run preview`. - diff --git a/frontend/TESTING.md b/frontend/TESTING.md index 8dc1af9..3df98af 100644 --- a/frontend/TESTING.md +++ b/frontend/TESTING.md @@ -3,6 +3,7 @@ ## What Was Implemented ### 1. Testing Infrastructure + - **Vitest** configuration with jsdom environment - **Global test setup** with mocked localStorage and environment variables - **Test scripts** in package.json for running tests in different modes @@ -11,12 +12,14 @@ ### 2. Test Categories #### Utility Function Tests (10 tests) + - `src/lib/utils.test.ts` - Tests for `cn()` class name utility (Tailwind CSS class merging) - Tests for `trim()` string utility function - Covers edge cases, conditional logic, and array/object inputs #### Validation Function Tests (22 tests) + - `src/lib/validation.test.ts` - Comprehensive validation for: - Email format validation @@ -27,12 +30,14 @@ - Covers both valid and invalid inputs with detailed error messages #### API Client Tests (4 tests) + - `src/lib/api/client.test.ts` - Tests API client configuration - Tests authorization middleware functionality - Mocks localStorage interactions for token management #### Data Loader Tests (20 tests) + - `src/routes/app/(components)/dataLoaders.test.ts` - Project creation API - `src/routes/auth/login/dataLoaders.test.ts` - User login API - `src/routes/auth/register/dataLoaders.test.ts` - User registration API @@ -42,11 +47,13 @@ ### 3. Test Utilities Created #### Validation Module + - `src/lib/validation.ts` - New utility module for form validation - Reusable validation functions with consistent error handling - Type-safe validation results with `ValidationResult` interface ### 4. Mock Strategy + - **localStorage**: Fully mocked for browser storage simulation - **API calls**: Mocked using Vitest's `vi.mock()` for predictable testing - **Environment variables**: Stubbed for consistent test environment @@ -81,4 +88,4 @@ pnpm test:ui # Run tests with coverage pnpm test:coverage -``` \ No newline at end of file +``` diff --git a/frontend/package.json b/frontend/package.json index 0213bdb..a1a12c9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,7 @@ "check": "svelte-kit sync && svelte-check --no-tsconfig --ignore \"**.test.ts\"", "check:watch": "svelte-kit sync && svelte-check --no-tsconfig --ignore \"**.test.ts\" --watch", "format": "prettier --write .", - "lint": "prettier --check . && eslint .", + "lint": "prettier --check . && eslint src --ignore-pattern \"src/**/*.test.ts\"", "test": "vitest", "test:run": "vitest run", "test:ui": "vitest --ui", diff --git a/frontend/src/lib/api/client.test.ts b/frontend/src/lib/api/client.test.ts index 83c94d4..d396ca4 100644 --- a/frontend/src/lib/api/client.test.ts +++ b/frontend/src/lib/api/client.test.ts @@ -8,7 +8,7 @@ vi.mock('openapi-fetch', () => ({ GET: vi.fn(), POST: vi.fn(), PUT: vi.fn(), - DELETE: vi.fn(), + DELETE: vi.fn() })) })); diff --git a/frontend/src/lib/api/client.ts b/frontend/src/lib/api/client.ts index b4b12a4..463936a 100644 --- a/frontend/src/lib/api/client.ts +++ b/frontend/src/lib/api/client.ts @@ -17,5 +17,4 @@ export const client = createClient({ baseUrl: API_BASE_URL }); - client.use(authWithHeadersMiddleware); diff --git a/frontend/src/lib/api/user.svelte.ts b/frontend/src/lib/api/user.svelte.ts index ed49d66..e03d890 100644 --- a/frontend/src/lib/api/user.svelte.ts +++ b/frontend/src/lib/api/user.svelte.ts @@ -9,7 +9,6 @@ interface JWTPayload { sub: string; // The subject of the JWT, typically username } - class UserState { user: UserSvelte | null = $state(null); token: string | null = $state(null); @@ -62,4 +61,4 @@ class UserState { } } -export const userState = new UserState(); \ No newline at end of file +export const userState = new UserState(); diff --git a/frontend/src/lib/components/ErrorAlert.svelte b/frontend/src/lib/components/ErrorAlert.svelte index 61af1ab..f37e283 100644 --- a/frontend/src/lib/components/ErrorAlert.svelte +++ b/frontend/src/lib/components/ErrorAlert.svelte @@ -9,8 +9,5 @@ Error - {trim(text, '"')} - - \ No newline at end of file + {trim(text, '"')} + diff --git a/frontend/src/lib/components/ui/alert/alert-description.svelte b/frontend/src/lib/components/ui/alert/alert-description.svelte index 8b56aed..5920ca6 100644 --- a/frontend/src/lib/components/ui/alert/alert-description.svelte +++ b/frontend/src/lib/components/ui/alert/alert-description.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/components/ui/dialog/dialog-content.svelte b/frontend/src/lib/components/ui/dialog/dialog-content.svelte index 99cc73b..c0e54b8 100644 --- a/frontend/src/lib/components/ui/dialog/dialog-content.svelte +++ b/frontend/src/lib/components/ui/dialog/dialog-content.svelte @@ -1,9 +1,9 @@ diff --git a/frontend/src/lib/components/ui/dialog/index.ts b/frontend/src/lib/components/ui/dialog/index.ts index dce1d9d..d9e5fb8 100644 --- a/frontend/src/lib/components/ui/dialog/index.ts +++ b/frontend/src/lib/components/ui/dialog/index.ts @@ -1,13 +1,13 @@ -import { Dialog as DialogPrimitive } from "bits-ui"; +import { Dialog as DialogPrimitive } from 'bits-ui'; -import Title from "./dialog-title.svelte"; -import Footer from "./dialog-footer.svelte"; -import Header from "./dialog-header.svelte"; -import Overlay from "./dialog-overlay.svelte"; -import Content from "./dialog-content.svelte"; -import Description from "./dialog-description.svelte"; -import Trigger from "./dialog-trigger.svelte"; -import Close from "./dialog-close.svelte"; +import Title from './dialog-title.svelte'; +import Footer from './dialog-footer.svelte'; +import Header from './dialog-header.svelte'; +import Overlay from './dialog-overlay.svelte'; +import Content from './dialog-content.svelte'; +import Description from './dialog-description.svelte'; +import Trigger from './dialog-trigger.svelte'; +import Close from './dialog-close.svelte'; const Root = DialogPrimitive.Root; const Portal = DialogPrimitive.Portal; @@ -33,5 +33,5 @@ export { Overlay as DialogOverlay, Content as DialogContent, Description as DialogDescription, - Close as DialogClose, + Close as DialogClose }; diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte index e03f949..e94c637 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -1,9 +1,9 @@ diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte index 64bb283..38bc45b 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -1,16 +1,16 @@ @@ -20,7 +20,7 @@ data-inset={inset} data-variant={variant} class={cn( - "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0", + "data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className )} {...restProps} diff --git a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte index f72e477..14e40f7 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +++ b/frontend/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/components/ui/dropdown-menu/index.ts b/frontend/src/lib/components/ui/dropdown-menu/index.ts index 1cf9f70..aeb398e 100644 --- a/frontend/src/lib/components/ui/dropdown-menu/index.ts +++ b/frontend/src/lib/components/ui/dropdown-menu/index.ts @@ -1,17 +1,17 @@ -import { DropdownMenu as DropdownMenuPrimitive } from "bits-ui"; -import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; -import Content from "./dropdown-menu-content.svelte"; -import Group from "./dropdown-menu-group.svelte"; -import Item from "./dropdown-menu-item.svelte"; -import Label from "./dropdown-menu-label.svelte"; -import RadioGroup from "./dropdown-menu-radio-group.svelte"; -import RadioItem from "./dropdown-menu-radio-item.svelte"; -import Separator from "./dropdown-menu-separator.svelte"; -import Shortcut from "./dropdown-menu-shortcut.svelte"; -import Trigger from "./dropdown-menu-trigger.svelte"; -import SubContent from "./dropdown-menu-sub-content.svelte"; -import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; -import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui'; +import CheckboxItem from './dropdown-menu-checkbox-item.svelte'; +import Content from './dropdown-menu-content.svelte'; +import Group from './dropdown-menu-group.svelte'; +import Item from './dropdown-menu-item.svelte'; +import Label from './dropdown-menu-label.svelte'; +import RadioGroup from './dropdown-menu-radio-group.svelte'; +import RadioItem from './dropdown-menu-radio-item.svelte'; +import Separator from './dropdown-menu-separator.svelte'; +import Shortcut from './dropdown-menu-shortcut.svelte'; +import Trigger from './dropdown-menu-trigger.svelte'; +import SubContent from './dropdown-menu-sub-content.svelte'; +import SubTrigger from './dropdown-menu-sub-trigger.svelte'; +import GroupHeading from './dropdown-menu-group-heading.svelte'; const Sub = DropdownMenuPrimitive.Sub; const Root = DropdownMenuPrimitive.Root; @@ -45,5 +45,5 @@ export { Sub, SubContent, SubTrigger, - Trigger, + Trigger }; diff --git a/frontend/src/lib/components/ui/sonner/index.ts b/frontend/src/lib/components/ui/sonner/index.ts index 1ad9f4a..fcaf06b 100644 --- a/frontend/src/lib/components/ui/sonner/index.ts +++ b/frontend/src/lib/components/ui/sonner/index.ts @@ -1 +1 @@ -export { default as Toaster } from "./sonner.svelte"; +export { default as Toaster } from './sonner.svelte'; diff --git a/frontend/src/lib/components/ui/sonner/sonner.svelte b/frontend/src/lib/components/ui/sonner/sonner.svelte index 1f50e1e..cb1f7c1 100644 --- a/frontend/src/lib/components/ui/sonner/sonner.svelte +++ b/frontend/src/lib/components/ui/sonner/sonner.svelte @@ -1,6 +1,6 @@ diff --git a/frontend/src/lib/components/ui/textarea/index.ts b/frontend/src/lib/components/ui/textarea/index.ts index ace797a..9ccb3bf 100644 --- a/frontend/src/lib/components/ui/textarea/index.ts +++ b/frontend/src/lib/components/ui/textarea/index.ts @@ -1,7 +1,7 @@ -import Root from "./textarea.svelte"; +import Root from './textarea.svelte'; export { Root, // - Root as Textarea, + Root as Textarea }; diff --git a/frontend/src/lib/components/ui/textarea/textarea.svelte b/frontend/src/lib/components/ui/textarea/textarea.svelte index 545b377..7504574 100644 --- a/frontend/src/lib/components/ui/textarea/textarea.svelte +++ b/frontend/src/lib/components/ui/textarea/textarea.svelte @@ -1,6 +1,6 @@ Create Project - - Fill in the details below to create a new project. - + Fill in the details below to create a new project.
@@ -63,7 +58,8 @@ - \ No newline at end of file + diff --git a/frontend/src/routes/app/(components)/dataLoaders.ts b/frontend/src/routes/app/(components)/dataLoaders.ts index c460bb8..3577674 100644 --- a/frontend/src/routes/app/(components)/dataLoaders.ts +++ b/frontend/src/routes/app/(components)/dataLoaders.ts @@ -15,4 +15,4 @@ export const createProject = async (title: string, description: string) => { } return data; -}; \ No newline at end of file +}; diff --git a/frontend/src/routes/app/+layout.svelte b/frontend/src/routes/app/+layout.svelte index aefdc6b..b8a6c37 100644 --- a/frontend/src/routes/app/+layout.svelte +++ b/frontend/src/routes/app/+layout.svelte @@ -45,7 +45,7 @@ />
- @@ -56,9 +56,9 @@ {#snippet child({ props })} - + {/snippet} @@ -76,19 +76,23 @@ window.open("https://github.com/IU-Capstone-Project-2025/ProjectOR", "_blank")}> + onclick={() => + window.open('https://github.com/IU-Capstone-Project-2025/ProjectOR', '_blank')} + > GitHub - window.open(`${API_BASE_URL}docs`, "_blank")}> + window.open(`${API_BASE_URL}docs`, '_blank')}> API - { - userState.logout(); - goto('/auth/login'); - }}> + { + userState.logout(); + goto('/auth/login'); + }} + > Log out diff --git a/frontend/src/routes/app/+page.svelte b/frontend/src/routes/app/+page.svelte index 9725a68..a0616b6 100644 --- a/frontend/src/routes/app/+page.svelte +++ b/frontend/src/routes/app/+page.svelte @@ -5,4 +5,4 @@ onMount(() => { goto('/app/explore'); }); - \ No newline at end of file + diff --git a/frontend/src/routes/app/explore/(components)/ProjectCard.svelte b/frontend/src/routes/app/explore/(components)/ProjectCard.svelte index 593d7c1..d978933 100644 --- a/frontend/src/routes/app/explore/(components)/ProjectCard.svelte +++ b/frontend/src/routes/app/explore/(components)/ProjectCard.svelte @@ -13,12 +13,10 @@ - -

{project.description}

+ +

{project.description}

- + - \ No newline at end of file + diff --git a/frontend/src/routes/app/explore/(components)/dataLoaders.ts b/frontend/src/routes/app/explore/(components)/dataLoaders.ts index 0703e04..47797fd 100644 --- a/frontend/src/routes/app/explore/(components)/dataLoaders.ts +++ b/frontend/src/routes/app/explore/(components)/dataLoaders.ts @@ -7,4 +7,4 @@ export const getProjects = async () => { throw new Error(JSON.stringify(error ?? 'Failed to fetch projects')); } return data; -}; \ No newline at end of file +}; diff --git a/frontend/src/routes/app/explore/+page.svelte b/frontend/src/routes/app/explore/+page.svelte index e5caa70..56dabdb 100644 --- a/frontend/src/routes/app/explore/+page.svelte +++ b/frontend/src/routes/app/explore/+page.svelte @@ -11,20 +11,18 @@ }); -
+
{#if $projectsQuery.isPending} - {#each Array(12) as _} + {#each Array(12) as _ (_)} {/each} {:else if $projectsQuery.isError} + {:else if $projectsQuery.data.length === 0} +

No projects found.

{:else} - {#if $projectsQuery.data.length === 0} -

No projects found.

- {:else} - {#each $projectsQuery.data as project} - - {/each} - {/if} + {#each $projectsQuery.data as project (project.title)} + + {/each} {/if}
diff --git a/frontend/src/routes/app/explore/+page.ts b/frontend/src/routes/app/explore/+page.ts index 6146690..60706f8 100644 --- a/frontend/src/routes/app/explore/+page.ts +++ b/frontend/src/routes/app/explore/+page.ts @@ -1,11 +1,11 @@ import type { PageLoad } from './$types'; import { getProjects } from './(components)/dataLoaders'; -export const load: PageLoad = async ({ parent, fetch }) => { +export const load: PageLoad = async ({ parent }) => { const { queryClient } = await parent(); await queryClient.prefetchQuery({ queryKey: ['projects'], queryFn: async () => await getProjects() }); -}; \ No newline at end of file +}; diff --git a/frontend/src/routes/auth/login/+page.svelte b/frontend/src/routes/auth/login/+page.svelte index 0e05422..2adbde5 100644 --- a/frontend/src/routes/auth/login/+page.svelte +++ b/frontend/src/routes/auth/login/+page.svelte @@ -42,12 +42,7 @@
- +
diff --git a/frontend/src/routes/auth/login/dataLoaders.test.ts b/frontend/src/routes/auth/login/dataLoaders.test.ts index 082afae..7441d6f 100644 --- a/frontend/src/routes/auth/login/dataLoaders.test.ts +++ b/frontend/src/routes/auth/login/dataLoaders.test.ts @@ -49,7 +49,7 @@ describe('Auth Data Loaders', () => { }; let capturedFormData: FormData | null = null; - + vi.mocked(client.POST).mockImplementationOnce(async (url, options) => { if (options?.bodySerializer) { const body = { @@ -97,9 +97,7 @@ describe('Auth Data Loaders', () => { error: mockError }); - await expect(login('', '')).rejects.toThrow( - JSON.stringify(mockError.detail) - ); + await expect(login('', '')).rejects.toThrow(JSON.stringify(mockError.detail)); }); it('should handle special characters in credentials', async () => { diff --git a/frontend/src/routes/auth/login/dataLoaders.ts b/frontend/src/routes/auth/login/dataLoaders.ts index c83d417..a0cf19e 100644 --- a/frontend/src/routes/auth/login/dataLoaders.ts +++ b/frontend/src/routes/auth/login/dataLoaders.ts @@ -23,4 +23,4 @@ export const login = async (username: string, password: string) => { } return data; -}; \ No newline at end of file +}; diff --git a/frontend/src/routes/auth/register/+page.svelte b/frontend/src/routes/auth/register/+page.svelte index 40cf187..bd10f89 100644 --- a/frontend/src/routes/auth/register/+page.svelte +++ b/frontend/src/routes/auth/register/+page.svelte @@ -18,7 +18,7 @@ confirmPassword: '' }); - const registerMutation = createMutation(({ + const registerMutation = createMutation({ mutationFn: async () => await register(username, password), onSuccess: (data) => { toast.success('Registration successful! Redirecting to login...'); @@ -28,7 +28,7 @@ onError: (error: Error) => { toast.error(`Registration failed: ${error.message}`); } - })); + });
@@ -43,12 +43,7 @@
- +
@@ -66,7 +61,10 @@