diff --git a/e2e/E2E_COVERAGE_REPORT.md b/e2e/E2E_COVERAGE_REPORT.md index d88e16c015..e66d417f16 100644 --- a/e2e/E2E_COVERAGE_REPORT.md +++ b/e2e/E2E_COVERAGE_REPORT.md @@ -1,6 +1,6 @@ # E2E Test Coverage Report -> **Last Updated:** 2026-04-23 +> **Last Updated:** 2026-04-22 > **Router Source:** [`react/src/routes.tsx`](../react/src/routes.tsx) > **E2E Root:** [`e2e/`](.) > @@ -12,42 +12,42 @@ **Scope:** Coverage metrics apply only to the routes listed below and do **not** include all entries from `react/src/routes.tsx`. Routes such as `/admin-dashboard` (not yet exposed in menu) and `/ai-agent` (experimental) are currently out of scope. -**Overall (in-scope routes): 261 / 408 features covered (64%)** - -| Page | Route | Features | Covered | Status | -| ----------------- | -------------------------------------- | :------: | :-----: | :-----: | -| Authentication | `/interactive-login` | 37 | 35 | 🔶 95% | -| Change Password | `/change-password` | 9 | 9 | ✅ 100% | -| Start Page | `/start` | 8 | 6 | 🔶 75% | -| Dashboard | `/dashboard` | 9 | 7 | 🔶 78% | -| Session List | `/session` | 22 | 14 | 🔶 64% | -| Session Launcher | `/session/start` | 14 | 3 | 🔶 21% | -| Serving | `/serving` | 7 | 2 | 🔶 29% | -| Endpoint Detail | `/serving/:serviceId` | 20 | 9 | 🔶 45% | -| Service Launcher | `/service/start` | 5 | 1 | 🔶 20% | -| VFolder / Data | `/data` | 45 | 32 | 🔶 71% | -| Model Store | `/model-store` | 6 | 6 | ✅ 100% | -| Admin Model Store | `/admin-model-store` | 22 | 22 | ✅ 100% | -| Storage Host | `/storage-settings/:hostname` | 3 | 0 | ❌ 0% | -| My Environment | `/my-environment` | 2 | 2 | ✅ 100% | -| Environment | `/environment` | 27 | 21 | 🔶 78% | -| Configurations | `/settings` | 10 | 8 | 🔶 80% | -| Resources | `/agent-summary`, `/agent` | 10 | 3 | 🔶 30% | -| Resource Policy | `/resource-policy` | 13 | 10 | 🔶 77% | -| User Credentials | `/credential` | 20 | 13 | 🔶 65% | -| Maintenance | `/maintenance` | 3 | 2 | 🔶 67% | -| User Settings | `/usersettings` | 10 | 1 | 🔶 10% | -| Project | `/project` | 6 | 5 | 🔶 83% | -| Statistics | `/statistics` | 2 | 2 | ✅ 100% | -| Scheduler | `/scheduler` | 6 | 0 | ❌ 0% | -| Information | `/information` | 2 | 2 | ✅ 100% | -| Reservoir | `/reservoir`, `/reservoir/:artifactId` | 18 | 0 | ❌ 0% | -| Branding | `/branding` | 14 | 0 | ❌ 0% | -| App Launcher | (modal) | 18 | 10 | 🔶 56% | -| Chat | `/chat/:id?` | 6 | 6 | ✅ 100% | -| Plugin System | (config-based) | 12 | 12 | ✅ 100% | -| RBAC Management | `/rbac` | 22 | 21 | 🔶 95% | -| **Total** | | **408** | **261** | **64%** | +**Overall (in-scope routes): 265 / 412 features covered (64%)** + +| Page | Route | Features | Covered | Status | +|------|-------|:--------:|:-------:|:------:| +| Authentication | `/interactive-login` | 37 | 35 | 🔶 95% | +| Change Password | `/change-password` | 9 | 9 | ✅ 100% | +| Start Page | `/start` | 8 | 6 | 🔶 75% | +| Dashboard | `/dashboard` | 9 | 7 | 🔶 78% | +| Session List | `/session` | 22 | 14 | 🔶 64% | +| Session Launcher | `/session/start` | 14 | 3 | 🔶 21% | +| Serving | `/serving` | 7 | 2 | 🔶 29% | +| Endpoint Detail | `/serving/:serviceId` | 20 | 9 | 🔶 45% | +| Service Launcher | `/service/start` | 5 | 1 | 🔶 20% | +| VFolder / Data | `/data` | 45 | 32 | 🔶 71% | +| Model Store | `/model-store` | 6 | 6 | ✅ 100% | +| Admin Model Store | `/admin-model-store` | 26 | 26 | ✅ 100% | +| Storage Host | `/storage-settings/:hostname` | 3 | 0 | ❌ 0% | +| My Environment | `/my-environment` | 2 | 2 | ✅ 100% | +| Environment | `/environment` | 27 | 21 | 🔶 78% | +| Configurations | `/settings` | 10 | 8 | 🔶 80% | +| Resources | `/agent-summary`, `/agent` | 10 | 3 | 🔶 30% | +| Resource Policy | `/resource-policy` | 13 | 10 | 🔶 77% | +| User Credentials | `/credential` | 20 | 13 | 🔶 65% | +| Maintenance | `/maintenance` | 3 | 2 | 🔶 67% | +| User Settings | `/usersettings` | 10 | 1 | 🔶 10% | +| Project | `/project` | 6 | 5 | 🔶 83% | +| Statistics | `/statistics` | 2 | 2 | ✅ 100% | +| Scheduler | `/scheduler` | 6 | 0 | ❌ 0% | +| Information | `/information` | 2 | 2 | ✅ 100% | +| Reservoir | `/reservoir`, `/reservoir/:artifactId` | 18 | 0 | ❌ 0% | +| Branding | `/branding` | 14 | 0 | ❌ 0% | +| App Launcher | (modal) | 18 | 10 | 🔶 56% | +| Chat | `/chat/:id?` | 6 | 6 | ✅ 100% | +| Plugin System | (config-based) | 12 | 12 | ✅ 100% | +| RBAC Management | `/rbac` | 22 | 21 | 🔶 95% | +| **Total** | | **412** | **265** | **64%** | --- @@ -68,43 +68,43 @@ **Test files:** [`e2e/auth/login.spec.ts`](auth/login.spec.ts), [`e2e/auth/password-expiry.spec.ts`](auth/password-expiry.spec.ts), [`e2e/auth/forgot-password.spec.ts`](auth/forgot-password.spec.ts), [`e2e/auth/concurrent-login-guard.spec.ts`](auth/concurrent-login-guard.spec.ts), [`e2e/auth/login-error-messages.spec.ts`](auth/login-error-messages.spec.ts) -| Feature | Status | Test | -| ---------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| Display login form | ✅ | `should display the login form` | -| Successful login & redirect | ✅ | `should redirect to the Summary` | -| Invalid email error | ✅ | `should display error message for non-existent email` | -| Invalid password error | ✅ | `should display error message for incorrect password` | -| Endpoint URL normalization (trailing slash) | ✅ | `user can login with endpoint that has a single trailing slash` | -| Endpoint URL normalization (multiple slashes) | ✅ | `user can login with endpoint that has multiple trailing slashes` | -| Endpoint URL normalization (double-slash prevention) | ✅ | `API requests do not contain double-slash after endpoint normalization` | -| Password expiry modal display | ✅ | `user sees the password change modal when their password has expired` | -| Password expiry modal not blocked by login | ✅ | `the login modal does not block the password change modal when password has expired` | -| Password expiry modal cancel | ✅ | `user can cancel the password change modal and return to the login form` | -| Password change empty validation | ✅ | `password change form shows a validation error when submitted empty` | -| Password change same-password rejection | ✅ | `password change form rejects a new password that is the same as the current one` | -| Full password change flow (real account) | ✅ | `user can complete the password change flow with a real account and re-login is attempted` | -| Forgot password modal open/close | ✅ | `User can open the forgot password modal from login page`, `User can close the modal and return to login form` | -| Forgot password email send success | ✅ | `User can send a password change email successfully` | -| Forgot password email send error | ✅ | `User sees an error when email sending fails` | -| Forgot password form validation (empty) | ✅ | `User cannot submit without email` | -| Forgot password form validation (invalid email) | ✅ | `User cannot submit with invalid email format` | -| Forgot password link config-driven visibility | ✅ | `"Forgot password?" link is hidden when config is disabled` | -| Concurrent session guard (409 modal) | ✅ | `user sees concurrent session modal when another session is active` | -| Concurrent session cancel & credential preservation | ✅ | `user can cancel concurrent session modal and return to login form with credentials preserved` | -| Force login (force=true) | ✅ | `clicking Proceed to Login sends a second login request with force=true` | -| Force login + TOTP persistence | ✅ | `TOTP is required after force login approval — force flag persists when submitting OTP` | -| Silent re-login skips concurrent modal | ✅ | `page refresh does not show concurrent session modal for silent re-login attempts` | -| Invalid API params error (missing username) | ✅ | `invalid API params (missing username) shows login failed notification` | -| Invalid API params error (missing password) | ✅ | `invalid API params (missing password) shows login failed notification` | -| Brute-force block (too many failures) | ✅ | `too many login failures shows brute-force block notification` | -| Auth failed — credential mismatch | ✅ | `credential mismatch shows login information mismatch notification` | -| Auth failed — inactive account | ✅ | `inactive account shows login information mismatch notification` | -| Auth failed — email verification required | ✅ | `email verification required shows email verification notification` | -| Auth failed — missing keypair | ✅ | `missing keypair shows login information mismatch notification` | -| Active login session exists notification | ✅ | `active login session exists shows session exists notification` | -| Monitor role login forbidden | ✅ | `monitor role user sees login forbidden notification` | -| OAuth/SSO login flow | ❌ | - | -| Session persistence | ❌ | - | +| Feature | Status | Test | +|---------|--------|------| +| Display login form | ✅ | `should display the login form` | +| Successful login & redirect | ✅ | `should redirect to the Summary` | +| Invalid email error | ✅ | `should display error message for non-existent email` | +| Invalid password error | ✅ | `should display error message for incorrect password` | +| Endpoint URL normalization (trailing slash) | ✅ | `user can login with endpoint that has a single trailing slash` | +| Endpoint URL normalization (multiple slashes) | ✅ | `user can login with endpoint that has multiple trailing slashes` | +| Endpoint URL normalization (double-slash prevention) | ✅ | `API requests do not contain double-slash after endpoint normalization` | +| Password expiry modal display | ✅ | `user sees the password change modal when their password has expired` | +| Password expiry modal not blocked by login | ✅ | `the login modal does not block the password change modal when password has expired` | +| Password expiry modal cancel | ✅ | `user can cancel the password change modal and return to the login form` | +| Password change empty validation | ✅ | `password change form shows a validation error when submitted empty` | +| Password change same-password rejection | ✅ | `password change form rejects a new password that is the same as the current one` | +| Full password change flow (real account) | ✅ | `user can complete the password change flow with a real account and re-login is attempted` | +| Forgot password modal open/close | ✅ | `User can open the forgot password modal from login page`, `User can close the modal and return to login form` | +| Forgot password email send success | ✅ | `User can send a password change email successfully` | +| Forgot password email send error | ✅ | `User sees an error when email sending fails` | +| Forgot password form validation (empty) | ✅ | `User cannot submit without email` | +| Forgot password form validation (invalid email) | ✅ | `User cannot submit with invalid email format` | +| Forgot password link config-driven visibility | ✅ | `"Forgot password?" link is hidden when config is disabled` | +| Concurrent session guard (409 modal) | ✅ | `user sees concurrent session modal when another session is active` | +| Concurrent session cancel & credential preservation | ✅ | `user can cancel concurrent session modal and return to login form with credentials preserved` | +| Force login (force=true) | ✅ | `clicking Proceed to Login sends a second login request with force=true` | +| Force login + TOTP persistence | ✅ | `TOTP is required after force login approval — force flag persists when submitting OTP` | +| Silent re-login skips concurrent modal | ✅ | `page refresh does not show concurrent session modal for silent re-login attempts` | +| Invalid API params error (missing username) | ✅ | `invalid API params (missing username) shows login failed notification` | +| Invalid API params error (missing password) | ✅ | `invalid API params (missing password) shows login failed notification` | +| Brute-force block (too many failures) | ✅ | `too many login failures shows brute-force block notification` | +| Auth failed — credential mismatch | ✅ | `credential mismatch shows login information mismatch notification` | +| Auth failed — inactive account | ✅ | `inactive account shows login information mismatch notification` | +| Auth failed — email verification required | ✅ | `email verification required shows email verification notification` | +| Auth failed — missing keypair | ✅ | `missing keypair shows login information mismatch notification` | +| Active login session exists notification | ✅ | `active login session exists shows session exists notification` | +| Monitor role login forbidden | ✅ | `monitor role user sees login forbidden notification` | +| OAuth/SSO login flow | ❌ | - | +| Session persistence | ❌ | - | **Coverage: 🔶 35/37 features** @@ -114,17 +114,17 @@ **Test files:** [`e2e/auth/forgot-password.spec.ts`](auth/forgot-password.spec.ts) -| Feature | Status | Test | -| --------------------------------------------- | ------ | ---------------------------------------------------------------------- | -| Display password change form with valid token | ✅ | `User sees the password change form with a valid token` | -| Successful password change | ✅ | `User can successfully change password with valid token` | -| Redirect to login after success | ✅ | `User is redirected to login page after closing the success modal` | -| Invalid token view (no token) | ✅ | `User sees invalid token view when accessing the page without a token` | -| Invalid token view (server rejection) | ✅ | `User sees invalid token view when server rejects the token` | -| Email mismatch error | ✅ | `User sees email mismatch error when email does not match the token` | -| Form validation (empty fields) | ✅ | `User cannot submit with empty fields` | -| Form validation (weak password) | ✅ | `User cannot submit with a weak password` | -| Form validation (password mismatch) | ✅ | `User cannot submit when passwords do not match` | +| Feature | Status | Test | +|---------|--------|------| +| Display password change form with valid token | ✅ | `User sees the password change form with a valid token` | +| Successful password change | ✅ | `User can successfully change password with valid token` | +| Redirect to login after success | ✅ | `User is redirected to login page after closing the success modal` | +| Invalid token view (no token) | ✅ | `User sees invalid token view when accessing the page without a token` | +| Invalid token view (server rejection) | ✅ | `User sees invalid token view when server rejects the token` | +| Email mismatch error | ✅ | `User sees email mismatch error when email does not match the token` | +| Form validation (empty fields) | ✅ | `User cannot submit with empty fields` | +| Form validation (weak password) | ✅ | `User cannot submit with a weak password` | +| Form validation (password mismatch) | ✅ | `User cannot submit when passwords do not match` | **Coverage: ✅ 9/9 features** @@ -136,16 +136,16 @@ **Modals:** `FolderCreateModal`, `StartFromURLModal` -| Feature | Status | Test | -| ---------------------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| Board layout rendering | ✅ | `Admin can see draggable cards on the Start page board` | -| Quick action: Create folder → FolderCreateModal | ✅ | `Admin can open the Create Folder modal from the Start page` / `Admin can create a folder from the Start page` | -| Quick action: Start interactive session → `/session/start` | ✅ | `Admin can navigate to the Session Launcher from the "Start Interactive Session" card` | -| Quick action: Start batch session → `/session/start` | ✅ | `Admin can navigate to the Session Launcher in batch mode` | -| Quick action: Start model service → `/service/start` | ✅ | `Admin can navigate to the Model Service creation page` | -| Quick action: Import from URL → StartFromURLModal | ✅ | `Admin can open the "Start From URL" modal from the Start page` | -| Board item drag & reorder | ❌ | - | -| VFolder invitation notifications | ❌ | - | +| Feature | Status | Test | +|---------|--------|------| +| Board layout rendering | ✅ | `Admin can see draggable cards on the Start page board` | +| Quick action: Create folder → FolderCreateModal | ✅ | `Admin can open the Create Folder modal from the Start page` / `Admin can create a folder from the Start page` | +| Quick action: Start interactive session → `/session/start` | ✅ | `Admin can navigate to the Session Launcher from the "Start Interactive Session" card` | +| Quick action: Start batch session → `/session/start` | ✅ | `Admin can navigate to the Session Launcher in batch mode` | +| Quick action: Start model service → `/service/start` | ✅ | `Admin can navigate to the Model Service creation page` | +| Quick action: Import from URL → StartFromURLModal | ✅ | `Admin can open the "Start From URL" modal from the Start page` | +| Board item drag & reorder | ❌ | - | +| VFolder invitation notifications | ❌ | - | **Coverage: 🔶 6/8 features** @@ -155,17 +155,17 @@ **Test files:** [`e2e/dashboard/dashboard.spec.ts`](dashboard/dashboard.spec.ts), visual regression: [`e2e/visual_regression/dashboard/dashboard_page.test.ts`](visual_regression/dashboard/dashboard_page.test.ts) -| Feature | Status | Test | -| ----------------------------------- | ------ | ---------------------------------------------------------------------------- | -| Dashboard rendering | ✅ | `Admin can see all expected dashboard widgets` | -| Session count cards | ✅ | `Admin can see session type breakdown in the session count widget` | -| Resource usage display (MyResource) | ✅ | `Admin can view CPU and Memory usage in the My Resources widget` | -| Resource usage per resource group | ✅ | `Admin can view resource usage scoped to the current resource group` | -| Agent statistics (admin) | ✅ | `Admin can view cluster-level resource statistics in the Agent Stats widget` | -| Active agents list (admin) | ❌ | - | -| Recent sessions list | ✅ | `Admin can view the recently created sessions list on the Dashboard` | -| Auto-refresh (15s) | ❌ | - | -| Dashboard item drag/resize | ✅ | `Admin can see resizable and movable widgets on the Dashboard` | +| Feature | Status | Test | +|---------|--------|------| +| Dashboard rendering | ✅ | `Admin can see all expected dashboard widgets` | +| Session count cards | ✅ | `Admin can see session type breakdown in the session count widget` | +| Resource usage display (MyResource) | ✅ | `Admin can view CPU and Memory usage in the My Resources widget` | +| Resource usage per resource group | ✅ | `Admin can view resource usage scoped to the current resource group` | +| Agent statistics (admin) | ✅ | `Admin can view cluster-level resource statistics in the Agent Stats widget` | +| Active agents list (admin) | ❌ | - | +| Recent sessions list | ✅ | `Admin can view the recently created sessions list on the Dashboard` | +| Auto-refresh (15s) | ❌ | - | +| Dashboard item drag/resize | ✅ | `Admin can see resizable and movable widgets on the Dashboard` | **Coverage: 🔶 7/9 features** @@ -179,30 +179,30 @@ **Sub-tabs:** Running | Finished **Modals/Drawers:** `TerminateSessionModal`, `SessionDetailDrawer` (via name click), `SessionSchedulingHistoryModal` -| Feature | Status | Test | -| -------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------ | -| Create interactive session (Start page) | ✅ | `User can create interactive session on the Start page` | -| Create batch session (Start page) | ✅ | `User can create batch session on the Start page` | -| Create interactive session (Session page) | ✅ | `User can create interactive session from the quick-action card` | -| Create batch session (Session page) | ✅ | Via session creation tests | -| Session lifecycle (create/monitor/terminate) | ✅ | `Create, monitor, and terminate interactive session` | -| Batch session auto-completion | ✅ | `Create and wait for batch session completion` | -| View container logs | ✅ | `View session container logs` | -| Monitor resource usage | ✅ | `Monitor session resource usage` | -| Status transitions | ✅ | `Session status transitions are correct` | -| Bulk terminate disabled for terminated | ✅ | `Cannot select terminated sessions for bulk operations` | -| Sensitive env vars cleared on reload | ✅ | `Sensitive environment variables are cleared` | -| Scheduling history modal | ✅ | `Session Scheduling History Modal` (via mocked GraphQL) | -| Session name click → SessionDetailDrawer | 🚧 | `Session detail drawer renders correctly and can show dependency info` (fixme: requires running agent) | -| Dependencies column toggle | ✅ | `Dependencies column can be enabled via table settings` | -| Session type filtering (interactive/batch/inference) | ❌ | - | -| Running/Finished status toggle | ❌ | - | -| Property filtering (name, resource group, agent) | ❌ | - | -| Session table sorting | ❌ | - | -| Pagination | ❌ | - | -| Batch terminate → TerminateSessionModal | ❌ | - | -| Scheduling history modal → SessionSchedulingHistoryModal | ✅ | `Admin can see the scheduling history button` + 18 more tests | -| Resource policy warnings | 🚧 | Skipped: `superadmin to modify keypair resource policy` | +| Feature | Status | Test | +| ---------------------------------------------------- | ------ | ---------------------------------------------------------- | +| Create interactive session (Start page) | ✅ | `User can create interactive session on the Start page` | +| Create batch session (Start page) | ✅ | `User can create batch session on the Start page` | +| Create interactive session (Session page) | ✅ | `User can create interactive session from the quick-action card` | +| Create batch session (Session page) | ✅ | Via session creation tests | +| Session lifecycle (create/monitor/terminate) | ✅ | `Create, monitor, and terminate interactive session` | +| Batch session auto-completion | ✅ | `Create and wait for batch session completion` | +| View container logs | ✅ | `View session container logs` | +| Monitor resource usage | ✅ | `Monitor session resource usage` | +| Status transitions | ✅ | `Session status transitions are correct` | +| Bulk terminate disabled for terminated | ✅ | `Cannot select terminated sessions for bulk operations` | +| Sensitive env vars cleared on reload | ✅ | `Sensitive environment variables are cleared` | +| Scheduling history modal | ✅ | `Session Scheduling History Modal` (via mocked GraphQL) | +| Session name click → SessionDetailDrawer | 🚧 | `Session detail drawer renders correctly and can show dependency info` (fixme: requires running agent) | +| Dependencies column toggle | ✅ | `Dependencies column can be enabled via table settings` | +| Session type filtering (interactive/batch/inference) | ❌ | - | +| Running/Finished status toggle | ❌ | - | +| Property filtering (name, resource group, agent) | ❌ | - | +| Session table sorting | ❌ | - | +| Pagination | ❌ | - | +| Batch terminate → TerminateSessionModal | ❌ | - | +| Scheduling history modal → SessionSchedulingHistoryModal | ✅ | `Admin can see the scheduling history button` + 18 more tests | +| Resource policy warnings | 🚧 | Skipped: `superadmin to modify keypair resource policy` | **Coverage: 🔶 14/22 features** @@ -215,22 +215,22 @@ **Steps:** 1.Session Type → 2.Environments & Resource → 3.Data & Storage → 4.Network → 5.Confirm **Modals:** `SessionTemplateModal` (recent history) -| Feature | Status | Test | -| -------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| Basic session creation | ✅ | Via session creation tests | -| Multi-step form navigation (5 steps) | ❌ | - | -| Environment/image selection | 🔶 | Partial (used in creation tests) | -| Resource allocation (CPU/memory/GPU) | ❌ | - | -| Resource presets | ❌ | - | -| HPC optimization settings | ❌ | - | -| VFolder mounting (Step 3) | ❌ | - | -| Port configuration (Step 4) | ❌ | - | -| Batch schedule/timeout options | ❌ | - | -| Session dependency via useStartSession | 🚧 | `Creates batch + interactive session with dependency` (fixme: requires running agent) | -| Session owner selection (admin) | ❌ | - | -| Form validation errors | ❌ | - | -| Cluster mode warning (multi-node x1) | 🔶 | `session-cluster-mode.spec.ts` (10 tests: 5 active, 5 skipped due to cluster-size limits/capacity constraints) | -| Session history → SessionTemplateModal | ✅ | `session-template-modal.spec.ts` (7 tests) | +| Feature | Status | Test | +| -------------------------------------- | ------ | -------------------------------- | +| Basic session creation | ✅ | Via session creation tests | +| Multi-step form navigation (5 steps) | ❌ | - | +| Environment/image selection | 🔶 | Partial (used in creation tests) | +| Resource allocation (CPU/memory/GPU) | ❌ | - | +| Resource presets | ❌ | - | +| HPC optimization settings | ❌ | - | +| VFolder mounting (Step 3) | ❌ | - | +| Port configuration (Step 4) | ❌ | - | +| Batch schedule/timeout options | ❌ | - | +| Session dependency via useStartSession | 🚧 | `Creates batch + interactive session with dependency` (fixme: requires running agent) | +| Session owner selection (admin) | ❌ | - | +| Form validation errors | ❌ | - | +| Cluster mode warning (multi-node x1) | 🔶 | `session-cluster-mode.spec.ts` (10 tests: 5 active, 5 skipped due to cluster-size limits/capacity constraints) | +| Session history → SessionTemplateModal | ✅ | `session-template-modal.spec.ts` (7 tests) | **Coverage: 🔶 3/14 features (most only indirectly tested)** @@ -245,15 +245,15 @@ **Table link:** Endpoint name → navigates to `/serving/:serviceId` **Row actions:** Edit → `/service/update/:endpointId`, Delete → confirm modal -| Feature | Status | Test | -| --------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------ | -| Endpoint list rendering | ✅ | `Admin can deploy a model service via ServiceLauncher UI` (verifies row visible in serving list) | -| "Start Service" → navigate to `/service/start` | ❌ | - | -| Endpoint name click → EndpointDetailPage | ❌ | - | -| Status filtering (Active/Destroyed) | ❌ | - | -| Property filtering | ❌ | - | -| Edit endpoint → navigate to `/service/update/:endpointId` | ❌ | - | -| Delete endpoint → confirm dialog | ✅ | `Admin can terminate a deployed service` | +| Feature | Status | Test | +| --------------------------------------------------------- | ------ | ---- | +| Endpoint list rendering | ✅ | `Admin can deploy a model service via ServiceLauncher UI` (verifies row visible in serving list) | +| "Start Service" → navigate to `/service/start` | ❌ | - | +| Endpoint name click → EndpointDetailPage | ❌ | - | +| Status filtering (Active/Destroyed) | ❌ | - | +| Property filtering | ❌ | - | +| Edit endpoint → navigate to `/service/update/:endpointId` | ❌ | - | +| Delete endpoint → confirm dialog | ✅ | `Admin can terminate a deployed service` | **Coverage: 🔶 2/7 features** @@ -267,28 +267,28 @@ **Modals:** `AutoScalingRuleEditorModal`, `EndpointTokenGenerationModal`, `BAIJSONViewerModal`, `SessionDetailDrawer`, `InferenceSessionErrorModal` **Mocks:** [`e2e/serving/mocking/endpoint-detail-mock.ts`](serving/mocking/endpoint-detail-mock.ts), [`e2e/serving/mocking/endpoint-list-mock.ts`](serving/mocking/endpoint-list-mock.ts) -| Feature | Status | Test | -| ------------------------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Service info display | ❌ | - | -| Edit button → navigate to `/service/update/:endpointId` | ❌ | - | -| "Add Rules" → AutoScalingRuleEditorModal (create) | ❌ | - | -| Edit scaling rule → AutoScalingRuleEditorModal (edit) | ❌ | - | -| Delete scaling rule → Popconfirm | ❌ | - | -| "Generate Token" → EndpointTokenGenerationModal | ❌ | - | -| Token list display | ❌ | - | -| Feature flag: route-node table toggle | ✅ | `1.1 Admin sees the new BAIRouteNodes table when route-node flag is enabled`, `1.2 Admin sees the legacy route table when route-node flag is disabled` | -| Routes table display (columns, tags, values) | ✅ | `4.1`–`4.7` (column headers, status tags, traffic tags, traffic ratio, session ID dash) | -| Route category toggle (Running/Finished) | ✅ | `2.1`–`2.3` (default Running, switch to Finished, switch back) | -| Route property filtering (Traffic Status) | ✅ | `3.1`–`3.4` (filter selector, filter by trafficStatus ACTIVE, filter by trafficStatus INACTIVE, remove filter) | -| Route table sorting | ✅ | `7.1`–`7.3` (sort by Status, sort by Traffic Ratio, Session ID no sorter) | -| Route table pagination | ✅ | `6.1`–`6.2` (total count display, navigate to page 2) | -| Route empty state | ✅ | `9.1`–`9.2` (empty Running, empty Finished) | -| Route error → BAIJSONViewerModal | ✅ | `5.1`–`5.3` (error icon, open modal with JSON, close modal) | -| Route session ID click → SessionDetailDrawer | ❌ | - | -| Session error → InferenceSessionErrorModal | ❌ | - | -| "Sync Routes" action | ✅ | `8.1`–`8.3` (button visible, success notification, error notification) | -| "Clear Errors" action | ❌ | - | -| Chat test link | ❌ | - | +| Feature | Status | Test | +| ------------------------------------------------------- | ------ | ---- | +| Service info display | ❌ | - | +| Edit button → navigate to `/service/update/:endpointId` | ❌ | - | +| "Add Rules" → AutoScalingRuleEditorModal (create) | ❌ | - | +| Edit scaling rule → AutoScalingRuleEditorModal (edit) | ❌ | - | +| Delete scaling rule → Popconfirm | ❌ | - | +| "Generate Token" → EndpointTokenGenerationModal | ❌ | - | +| Token list display | ❌ | - | +| Feature flag: route-node table toggle | ✅ | `1.1 Admin sees the new BAIRouteNodes table when route-node flag is enabled`, `1.2 Admin sees the legacy route table when route-node flag is disabled` | +| Routes table display (columns, tags, values) | ✅ | `4.1`–`4.7` (column headers, status tags, traffic tags, traffic ratio, session ID dash) | +| Route category toggle (Running/Finished) | ✅ | `2.1`–`2.3` (default Running, switch to Finished, switch back) | +| Route property filtering (Traffic Status) | ✅ | `3.1`–`3.4` (filter selector, filter by trafficStatus ACTIVE, filter by trafficStatus INACTIVE, remove filter) | +| Route table sorting | ✅ | `7.1`–`7.3` (sort by Status, sort by Traffic Ratio, Session ID no sorter) | +| Route table pagination | ✅ | `6.1`–`6.2` (total count display, navigate to page 2) | +| Route empty state | ✅ | `9.1`–`9.2` (empty Running, empty Finished) | +| Route error → BAIJSONViewerModal | ✅ | `5.1`–`5.3` (error icon, open modal with JSON, close modal) | +| Route session ID click → SessionDetailDrawer | ❌ | - | +| Session error → InferenceSessionErrorModal | ❌ | - | +| "Sync Routes" action | ✅ | `8.1`–`8.3` (button visible, success notification, error notification) | +| "Clear Errors" action | ❌ | - | +| Chat test link | ❌ | - | **Coverage: 🔶 9/20 features** @@ -298,13 +298,13 @@ **Test files:** [`e2e/serving/serving-deploy-lifecycle.spec.ts`](serving/serving-deploy-lifecycle.spec.ts) (integration, `@integration @serving`) -| Feature | Status | Test | -| ----------------------- | ------ | --------------------------------------------------------- | -| Create model service | ✅ | `Admin can deploy a model service via ServiceLauncher UI` | -| Update existing service | ❌ | - | -| Resource configuration | ❌ | - | -| Model folder selection | ❌ | - | -| Form validation | ❌ | - | +| Feature | Status | Test | +| ----------------------- | ------ | ---- | +| Create model service | ✅ | `Admin can deploy a model service via ServiceLauncher UI` | +| Update existing service | ❌ | - | +| Resource configuration | ❌ | - | +| Model folder selection | ❌ | - | +| Form validation | ❌ | - | **Coverage: 🔶 1/5 features** @@ -322,53 +322,53 @@ **Bulk actions (Deleted):** Restore → `RestoreVFolderModal` **Row actions:** Share → `InviteFolderSettingModal`, Permission info → `SharedFolderPermissionInfoModal` -| Feature | Status | Test | -| ---------------------------------------------------------- | ------ | ------------------------------------------------------------------------------ | -| Create folder (default) → FolderCreateModal | ✅ | `User can create default vFolder` | -| Create folder (specific location) → FolderCreateModal | ✅ | `User can create a vFolder by selecting a specific location` | -| Create model folder → FolderCreateModal | ✅ | `User can create Model vFolder` | -| Create cloneable model folder | ✅ | `User can create cloneable Model vFolder` | -| Create R/W folder | ✅ | `User can create Read & Write vFolder` | -| Create R/O folder | ✅ | `User can create Read Only vFolder` | -| Create auto-mount folder | ✅ | `User can create Auto Mount vFolder` | -| Delete / trash / restore / purge | ✅ | `User can create, delete(move to trash), restore, delete forever` | -| Consecutive deletion | ✅ | `User can create and permanently delete multiple VFolders` | -| Share folder → InviteFolderSettingModal | ✅ | `User can share vFolder` | -| File upload (button) | ✅ | `User can upload a single/multiple files via Upload button` | -| File upload (drag & drop) | ✅ | `User can upload a file via drag and drop` | -| File upload (duplicate handling) | ✅ | `User sees duplicate confirmation` / `User can cancel duplicate` | -| File upload (permissions) | ✅ | `User cannot upload files to read-only VFolder` | -| File upload (subdirectory) | ✅ | `User can upload a file to a subdirectory` | -| Explorer modal (CRUD) | ✅ | `User can create folders and upload files` | -| Explorer modal (read-only) | ✅ | `User can view files but cannot upload to read-only` | -| Explorer modal (error handling) | ✅ | `User sees error message when accessing non-existent` | -| Explorer modal (open/close) | ✅ | `User can open and close VFolder explorer modal` | -| Explorer modal (file browser) | ✅ | `User can access File Browser from VFolder explorer` | -| Explorer modal (details view) | ✅ | `User can view VFolder details in the explorer` | -| File creation (Create File button) | ✅ | `User can see Create File button in file explorer` | -| File creation (new file) | ✅ | `User can create a new file in the file explorer` | -| File creation (yaml config) | ✅ | `User can create a yaml configuration file` | -| File creation (empty name validation) | ✅ | `User cannot create a file with empty name` | -| File creation (invalid chars validation) | ✅ | `User cannot create a file with invalid characters in name` | -| File creation (read-only disabled) | 🚧 | Skipped: `User cannot create files in read-only VFolder` | -| Type selection: User-type default | ✅ | `User can create a User-type vfolder with default selection` | -| Type selection: Project-type (admin) | ✅ | `Admin can create a Project-type vfolder` | -| Type selection: Project disabled for model mode | ✅ | `Project radio is disabled when usage mode is model (non-model-store project)` | -| Type selection: Project disabled for automount | ✅ | `Project radio is disabled when usage mode is automount` | -| Type selection: Project enabled for general | ✅ | `Project radio is enabled when usage mode is general` | -| Type selection: User-only for regular user | ✅ | `Regular user sees only User-type radio (no Project radio)` | -| Type selection: Both types for admin | ✅ | `Admin sees both User-type and Project-type radios` | -| Active/Deleted tab switching | ❌ | - | -| Usage mode filtering (general/pipeline/automount/model) | ❌ | - | -| Property filtering (name, status, location) | ❌ | - | -| Folder table sorting | ❌ | - | -| Pagination | ❌ | - | -| Storage status / quota display | ❌ | - | -| Bulk trash → DeleteVFolderModal | ❌ | - | -| Bulk restore → RestoreVFolderModal | ❌ | - | -| Invitation notifications | ❌ | - | -| Shared folder permission → SharedFolderPermissionInfoModal | ❌ | - | -| File download | ❌ | - | +| Feature | Status | Test | +| ---------------------------------------------------------- | ------ | ----------------------------------------------------------------- | +| Create folder (default) → FolderCreateModal | ✅ | `User can create default vFolder` | +| Create folder (specific location) → FolderCreateModal | ✅ | `User can create a vFolder by selecting a specific location` | +| Create model folder → FolderCreateModal | ✅ | `User can create Model vFolder` | +| Create cloneable model folder | ✅ | `User can create cloneable Model vFolder` | +| Create R/W folder | ✅ | `User can create Read & Write vFolder` | +| Create R/O folder | ✅ | `User can create Read Only vFolder` | +| Create auto-mount folder | ✅ | `User can create Auto Mount vFolder` | +| Delete / trash / restore / purge | ✅ | `User can create, delete(move to trash), restore, delete forever` | +| Consecutive deletion | ✅ | `User can create and permanently delete multiple VFolders` | +| Share folder → InviteFolderSettingModal | ✅ | `User can share vFolder` | +| File upload (button) | ✅ | `User can upload a single/multiple files via Upload button` | +| File upload (drag & drop) | ✅ | `User can upload a file via drag and drop` | +| File upload (duplicate handling) | ✅ | `User sees duplicate confirmation` / `User can cancel duplicate` | +| File upload (permissions) | ✅ | `User cannot upload files to read-only VFolder` | +| File upload (subdirectory) | ✅ | `User can upload a file to a subdirectory` | +| Explorer modal (CRUD) | ✅ | `User can create folders and upload files` | +| Explorer modal (read-only) | ✅ | `User can view files but cannot upload to read-only` | +| Explorer modal (error handling) | ✅ | `User sees error message when accessing non-existent` | +| Explorer modal (open/close) | ✅ | `User can open and close VFolder explorer modal` | +| Explorer modal (file browser) | ✅ | `User can access File Browser from VFolder explorer` | +| Explorer modal (details view) | ✅ | `User can view VFolder details in the explorer` | +| File creation (Create File button) | ✅ | `User can see Create File button in file explorer` | +| File creation (new file) | ✅ | `User can create a new file in the file explorer` | +| File creation (yaml config) | ✅ | `User can create a yaml configuration file` | +| File creation (empty name validation) | ✅ | `User cannot create a file with empty name` | +| File creation (invalid chars validation) | ✅ | `User cannot create a file with invalid characters in name` | +| File creation (read-only disabled) | 🚧 | Skipped: `User cannot create files in read-only VFolder` | +| Type selection: User-type default | ✅ | `User can create a User-type vfolder with default selection` | +| Type selection: Project-type (admin) | ✅ | `Admin can create a Project-type vfolder` | +| Type selection: Project disabled for model mode | ✅ | `Project radio is disabled when usage mode is model (non-model-store project)` | +| Type selection: Project disabled for automount | ✅ | `Project radio is disabled when usage mode is automount` | +| Type selection: Project enabled for general | ✅ | `Project radio is enabled when usage mode is general` | +| Type selection: User-only for regular user | ✅ | `Regular user sees only User-type radio (no Project radio)` | +| Type selection: Both types for admin | ✅ | `Admin sees both User-type and Project-type radios` | +| Active/Deleted tab switching | ❌ | - | +| Usage mode filtering (general/pipeline/automount/model) | ❌ | - | +| Property filtering (name, status, location) | ❌ | - | +| Folder table sorting | ❌ | - | +| Pagination | ❌ | - | +| Storage status / quota display | ❌ | - | +| Bulk trash → DeleteVFolderModal | ❌ | - | +| Bulk restore → RestoreVFolderModal | ❌ | - | +| Invitation notifications | ❌ | - | +| Shared folder permission → SharedFolderPermissionInfoModal | ❌ | - | +| File download | ❌ | - | **Coverage: 🔶 32/45 features (includes 1 skipped)** @@ -380,14 +380,14 @@ **Drawer:** `ModelCardDrawer` (card click), **Modal:** `ModelCardDeployModal` (deploy) -| Feature | Status | Test | -| ----------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | -| Model card list rendering | ✅ | `admin can open model card drawer by clicking a card` | -| Model card drawer metadata | ✅ | `admin can see model description / metadata tags / metadata table / README content in the drawer` | -| Deploy button disabled (no presets) | ✅ | `admin cannot deploy when model card has no presets` | -| Deploy modal (multi-preset) | ✅ | `admin can open the Deploy Model modal / see preset options grouped by runtime variant / deploy after selection` | -| Auto-deploy (single preset + RG) | ✅ | `admin can auto-deploy when single preset and resource group available` | -| Post-deploy alerts | ✅ | `admin can see "Preparing your service" / "Service Ready" alerts on EndpointDetailPage` | +| Feature | Status | Test | +| --------------------------------- | ------ | ---- | +| Model card list rendering | ✅ | `admin can open model card drawer by clicking a card` | +| Model card drawer metadata | ✅ | `admin can see model description / metadata tags / metadata table / README content in the drawer` | +| Deploy button disabled (no presets) | ✅ | `admin cannot deploy when model card has no presets` | +| Deploy modal (multi-preset) | ✅ | `admin can open the Deploy Model modal / see preset options grouped by runtime variant / deploy after selection` | +| Auto-deploy (single preset + RG) | ✅ | `admin can auto-deploy when single preset and resource group available` | +| Post-deploy alerts | ✅ | `admin can see "Preparing your service" / "Service Ready" alerts on EndpointDetailPage` | **Coverage: ✅ 6/6 features** @@ -402,32 +402,36 @@ **Row actions:** Edit (setting icon), Delete (trash icon) **Bulk actions:** Bulk delete via header checkbox selection -| Feature | Status | Test | -| ---------------------------------------------- | ------ | ----------------------------------------- | -| Page load and table rendering | ✅ | `admin-model-card-page-load.spec.ts` | -| Column visibility and pagination | ✅ | `admin-model-card-page-load.spec.ts` | -| Name filter search | ✅ | `admin-model-card-filter.spec.ts` | -| Filter clear and empty state | ✅ | `admin-model-card-filter.spec.ts` | -| Open create modal | ✅ | `admin-model-card-create.spec.ts` | -| Create with required fields only | ✅ | `admin-model-card-create.spec.ts` | -| Create with all fields | ✅ | `admin-model-card-create.spec.ts` | -| Create validation (name required) | ✅ | `admin-model-card-create.spec.ts` | -| Create validation (VFolder required) | ✅ | `admin-model-card-create.spec.ts` | -| Cancel create modal | ✅ | `admin-model-card-create.spec.ts` | -| Open edit modal | ✅ | `admin-model-card-edit.spec.ts` | -| Update model card fields | ✅ | `admin-model-card-edit.spec.ts` | -| Edit validation | ✅ | `admin-model-card-edit.spec.ts` | -| Cancel edit modal | ✅ | `admin-model-card-edit.spec.ts` | -| Single delete with confirmation | ✅ | `admin-model-card-delete.spec.ts` | -| Cancel single delete | ✅ | `admin-model-card-delete.spec.ts` | -| Bulk select and delete | ✅ | `admin-model-card-delete.spec.ts` | -| Cancel bulk delete | ✅ | `admin-model-card-delete.spec.ts` | -| Clear selection | ✅ | `admin-model-card-delete.spec.ts` | -| Select all via header checkbox | ✅ | `admin-model-card-delete.spec.ts` | -| Non-admin access blocked | ✅ | `admin-model-card-access-control.spec.ts` | -| URL state persistence (filter/sort/pagination) | ✅ | `admin-model-card-url-state.spec.ts` | - -**Coverage: ✅ 22/22 features** +| Feature | Status | Test | +|---------|--------|------| +| Page load and table rendering | ✅ | `admin-model-card-page-load.spec.ts` | +| Column visibility and pagination | ✅ | `admin-model-card-page-load.spec.ts` | +| Name filter search | ✅ | `admin-model-card-filter.spec.ts` | +| Filter clear and empty state | ✅ | `admin-model-card-filter.spec.ts` | +| Open create modal | ✅ | `admin-model-card-create.spec.ts` | +| Create with required fields only | ✅ | `admin-model-card-create.spec.ts` | +| Create with all fields | ✅ | `admin-model-card-create.spec.ts` | +| Create validation (name required) | ✅ | `admin-model-card-create.spec.ts` | +| Create validation (VFolder required) | ✅ | `admin-model-card-create.spec.ts` | +| Cancel create modal | ✅ | `admin-model-card-create.spec.ts` | +| Open edit modal | ✅ | `admin-model-card-edit.spec.ts` | +| Update model card fields | ✅ | `admin-model-card-edit.spec.ts` | +| Edit validation | ✅ | `admin-model-card-edit.spec.ts` | +| Cancel edit modal | ✅ | `admin-model-card-edit.spec.ts` | +| Single delete with confirmation | ✅ | `admin-model-card-delete.spec.ts` | +| Cancel single delete | ✅ | `admin-model-card-delete.spec.ts` | +| Delete card + folder together (checkbox) | ✅ | `admin-model-card-delete.spec.ts` | +| Notification + Go to Trash with folder filter | ✅ | `admin-model-card-delete.spec.ts` | +| Delete card only, folder kept notification | ✅ | `admin-model-card-delete.spec.ts` | +| Go to Trash without folder filter | ✅ | `admin-model-card-delete.spec.ts` | +| Bulk select and delete | ✅ | `admin-model-card-delete.spec.ts` | +| Cancel bulk delete | ✅ | `admin-model-card-delete.spec.ts` | +| Clear selection | ✅ | `admin-model-card-delete.spec.ts` | +| Select all via header checkbox | ✅ | `admin-model-card-delete.spec.ts` | +| Non-admin access blocked | ✅ | `admin-model-card-access-control.spec.ts` | +| URL state persistence (filter/sort/pagination) | ✅ | `admin-model-card-url-state.spec.ts` | + +**Coverage: ✅ 26/26 features** --- @@ -435,11 +439,11 @@ **Test files:** None -| Feature | Status | Test | +| Feature | Status | Test | | -------------------- | ------ | ---- | -| Storage host details | ❌ | - | -| Resource panel | ❌ | - | -| Quota settings | ❌ | - | +| Storage host details | ❌ | - | +| Resource panel | ❌ | - | +| Quota settings | ❌ | - | **Coverage: ❌ 0/3 features** @@ -449,10 +453,10 @@ **Test files:** [`e2e/my-environment/my-environment.spec.ts`](my-environment/my-environment.spec.ts) -| Feature | Status | Test | -| ------------------------- | ------ | ------------------------------------------------------ | -| Custom image list | ✅ | `User can see custom image list with expected columns` | -| Image management (search) | ✅ | `User can search custom images` | +| Feature | Status | Test | +|---------|--------|------| +| Custom image list | ✅ | `User can see custom image list with expected columns` | +| Image management (search) | ✅ | `User can search custom images` | **Coverage: ✅ 2/2 features** @@ -469,51 +473,51 @@ **Row actions:** `ImageInstallModal`, `ManageAppsModal`, `ManageImageResourceLimitModal` **Filter:** `BAIPropertyFilter` (Name, Architecture, Status, Type, Registry) -| Feature | Status | Test | +| Feature | Status | Test | | ---------------------------------------------------- | ------ | --------------------------------------------------------------------------- | -| Image list rendering | ✅ | `Rendering Image List` | -| Image resource limit → ManageImageResourceLimitModal | ✅ | `user can modify image resource limit` | -| Image app management → ManageAppsModal | ✅ | `user can manage apps` | -| Image installation → ImageInstallModal | 🚧 | Skipped: `user can install image` | -| BAIPropertyFilter UI rendering | ✅ | `Admin can see the BAIPropertyFilter on the Images tab` | -| Filter by name (free text) | ✅ | `Admin can filter images by name using a text value` | -| Filter by architecture (strict selection) | ✅ | `Admin can filter images by architecture using strict selection` | -| Filter by status (strict selection) | ✅ | `Admin can filter images by status using strict selection` | -| Filter by type (strict selection) | ✅ | `Admin can filter images by type using strict selection` | -| Filter by registry (free text) | ✅ | `Admin can filter images by registry using a text value` | -| Multiple filters with reset-all | ✅ | `Admin can apply multiple filters simultaneously and see reset-all button` | -| Clear single filter tag | ✅ | `Admin can clear a single filter tag by clicking its close button` | -| Clear all filters (reset-all button) | ✅ | `Admin can clear all filters at once using the reset-all button` | -| Pagination reset on filter | ✅ | `Admin sees pagination reset to page 1 when a filter is applied on page 2` | -| Strict selection rejects freeform | ✅ | `Admin cannot add a filter for architecture with an invalid freeform value` | -| Empty state for non-matching filter | ✅ | `Admin sees empty state when filtering by a non-existent image name` | -| Table column settings → TableColumnsSettingModal | ❌ | - | +| Image list rendering | ✅ | `Rendering Image List` | +| Image resource limit → ManageImageResourceLimitModal | ✅ | `user can modify image resource limit` | +| Image app management → ManageAppsModal | ✅ | `user can manage apps` | +| Image installation → ImageInstallModal | 🚧 | Skipped: `user can install image` | +| BAIPropertyFilter UI rendering | ✅ | `Admin can see the BAIPropertyFilter on the Images tab` | +| Filter by name (free text) | ✅ | `Admin can filter images by name using a text value` | +| Filter by architecture (strict selection) | ✅ | `Admin can filter images by architecture using strict selection` | +| Filter by status (strict selection) | ✅ | `Admin can filter images by status using strict selection` | +| Filter by type (strict selection) | ✅ | `Admin can filter images by type using strict selection` | +| Filter by registry (free text) | ✅ | `Admin can filter images by registry using a text value` | +| Multiple filters with reset-all | ✅ | `Admin can apply multiple filters simultaneously and see reset-all button` | +| Clear single filter tag | ✅ | `Admin can clear a single filter tag by clicking its close button` | +| Clear all filters (reset-all button) | ✅ | `Admin can clear all filters at once using the reset-all button` | +| Pagination reset on filter | ✅ | `Admin sees pagination reset to page 1 when a filter is applied on page 2` | +| Strict selection rejects freeform | ✅ | `Admin cannot add a filter for architecture with an invalid freeform value` | +| Empty state for non-matching filter | ✅ | `Admin sees empty state when filtering by a non-existent image name` | +| Table column settings → TableColumnsSettingModal | ❌ | - | #### Resource Presets Tab **Primary action:** "+" → `ResourcePresetSettingModal` **Row actions:** Edit → `ResourcePresetSettingModal`, Delete → Popconfirm -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------------------------ | ------ | ---- | -| Preset list rendering | ❌ | - | -| Create preset → ResourcePresetSettingModal | ❌ | - | -| Edit preset → ResourcePresetSettingModal | ❌ | - | -| Delete preset → Popconfirm | ❌ | - | +| Preset list rendering | ❌ | - | +| Create preset → ResourcePresetSettingModal | ❌ | - | +| Edit preset → ResourcePresetSettingModal | ❌ | - | +| Delete preset → Popconfirm | ❌ | - | #### Container Registries Tab (superadmin) **Primary action:** "+" → `ContainerRegistryEditorModal` **Row actions:** Edit → `ContainerRegistryEditorModal`, Delete → Popconfirm, Enable/Disable toggle -| Feature | Status | Test | +| Feature | Status | Test | | ---------------------------------------------- | ------ | -------------------------------------------------------------- | -| Registry list rendering | ✅ | `Admin can see the registry table with all expected columns` | -| Create registry → ContainerRegistryEditorModal | ✅ | `Admin can add a new registry with required fields only` | -| Edit registry → ContainerRegistryEditorModal | ✅ | `Admin can edit the registry URL and project name` | -| Delete registry → Popconfirm | ✅ | `Admin can delete the registry with correct name confirmation` | -| Enable/disable registry toggle | ✅ | `Registry Control Operations` suite | -| Registry filtering / search | ✅ | `Registry Filtering` suite | +| Registry list rendering | ✅ | `Admin can see the registry table with all expected columns` | +| Create registry → ContainerRegistryEditorModal | ✅ | `Admin can add a new registry with required fields only` | +| Edit registry → ContainerRegistryEditorModal | ✅ | `Admin can edit the registry URL and project name` | +| Delete registry → Popconfirm | ✅ | `Admin can delete the registry with correct name confirmation` | +| Enable/disable registry toggle | ✅ | `Registry Control Operations` suite | +| Registry filtering / search | ✅ | `Registry Filtering` suite | **Coverage: 🔶 21/27 features** @@ -525,18 +529,18 @@ **Modals:** `OverlayNetworkSettingModal`, `SchedulerSettingModal` -| Feature | Status | Test | +| Feature | Status | Test | | ---------------------------------------------------- | ------ | ------------------------------------------- | -| Block list menu hiding | ✅ | `block list` | -| Inactive list menu disabling | ✅ | `inactiveList` | -| 404 for blocked pages | ✅ | `404 page when accessing blocklisted pages` | -| 401 for unauthorized pages | ✅ | `Regular user sees 401 page` | -| Root redirect with blocklist | ✅ | `redirected to first available page` | -| Combined blocklist + inactiveList | ✅ | `correct behavior when both configured` | -| Config clear restore behavior | ✅ | `Configuration can be cleared to restore` | -| showNonInstalledImages setting | ✅ | `showNonInstalledImages` | -| Overlay network setting → OverlayNetworkSettingModal | ❌ | - | -| Scheduler setting → SchedulerSettingModal | ❌ | - | +| Block list menu hiding | ✅ | `block list` | +| Inactive list menu disabling | ✅ | `inactiveList` | +| 404 for blocked pages | ✅ | `404 page when accessing blocklisted pages` | +| 401 for unauthorized pages | ✅ | `Regular user sees 401 page` | +| Root redirect with blocklist | ✅ | `redirected to first available page` | +| Combined blocklist + inactiveList | ✅ | `correct behavior when both configured` | +| Config clear restore behavior | ✅ | `Configuration can be cleared to restore` | +| showNonInstalledImages setting | ✅ | `showNonInstalledImages` | +| Overlay network setting → OverlayNetworkSettingModal | ❌ | - | +| Scheduler setting → SchedulerSettingModal | ❌ | - | **Coverage: 🔶 8/10 features** @@ -550,25 +554,25 @@ #### Agent Summary (`/agent-summary`) -| Feature | Status | Test | -| ------------------------------------- | ------ | ---------------------------------------------------------- | -| Agent Summary list with columns | ✅ | `Admin can see Agent Summary page with expected columns` | -| Connected/Terminated filter switching | ✅ | `Admin can switch between Connected and Terminated agents` | +| Feature | Status | Test | +|---------|--------|------| +| Agent Summary list with columns | ✅ | `Admin can see Agent Summary page with expected columns` | +| Connected/Terminated filter switching | ✅ | `Admin can switch between Connected and Terminated agents` | #### Agents Tab **Table link:** Agent name → `AgentDetailDrawer` -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------------------ | ------ | ------------------------------------------ | -| Agent list with connected agents | ✅ | `should have at least one connected agent` | -| Agent name click → AgentDetailDrawer | ❌ | - | +| Agent list with connected agents | ✅ | `should have at least one connected agent` | +| Agent name click → AgentDetailDrawer | ❌ | - | #### Storage Proxies Tab -| Feature | Status | Test | +| Feature | Status | Test | | ---------------------------- | ------ | ---- | -| Storage proxy list rendering | ❌ | - | +| Storage proxy list rendering | ❌ | - | #### Resource Groups Tab @@ -576,13 +580,13 @@ **Table link:** Name → `ResourceGroupInfoModal` **Row actions:** Edit → `ResourceGroupSettingModal`, Delete → Popconfirm -| Feature | Status | Test | +| Feature | Status | Test | | -------------------------------------------------- | ------ | ---- | -| Resource group list rendering | ❌ | - | -| Create resource group → ResourceGroupSettingModal | ❌ | - | -| Resource group name click → ResourceGroupInfoModal | ❌ | - | -| Edit resource group → ResourceGroupSettingModal | ❌ | - | -| Delete resource group → Popconfirm | ❌ | - | +| Resource group list rendering | ❌ | - | +| Create resource group → ResourceGroupSettingModal | ❌ | - | +| Resource group name click → ResourceGroupInfoModal | ❌ | - | +| Edit resource group → ResourceGroupSettingModal | ❌ | - | +| Delete resource group → Popconfirm | ❌ | - | **Coverage: 🔶 3/10 features** @@ -600,37 +604,37 @@ **Table link:** Info icon → `KeypairResourcePolicyInfoModal` **Row actions:** Edit → `KeypairResourcePolicySettingModal`, Delete → mutation -| Feature | Status | Test | -| --------------------------------------------------------- | ------ | --------------------------------------------------------- | -| Keypair policy list rendering | ✅ | `Admin can see Keypair policy list with expected columns` | -| Create keypair policy → KeypairResourcePolicySettingModal | ✅ | `Admin can create a Keypair policy` | -| View keypair policy → KeypairResourcePolicyInfoModal | ❌ | - | -| Edit keypair policy → KeypairResourcePolicySettingModal | ✅ | `Admin can edit a Keypair policy` | -| Delete keypair policy | ✅ | `Admin can delete a Keypair policy` | +| Feature | Status | Test | +|---------|--------|------| +| Keypair policy list rendering | ✅ | `Admin can see Keypair policy list with expected columns` | +| Create keypair policy → KeypairResourcePolicySettingModal | ✅ | `Admin can create a Keypair policy` | +| View keypair policy → KeypairResourcePolicyInfoModal | ❌ | - | +| Edit keypair policy → KeypairResourcePolicySettingModal | ✅ | `Admin can edit a Keypair policy` | +| Delete keypair policy | ✅ | `Admin can delete a Keypair policy` | #### User Policies Tab **Primary action:** "+" → `UserResourcePolicySettingModal` **Row actions:** Edit → `UserResourcePolicySettingModal`, Delete → Popconfirm -| Feature | Status | Test | -| --------------------------------------------------- | ------ | -------------------------------- | -| User policy list rendering | ✅ | `Admin can see User policy list` | -| Create user policy → UserResourcePolicySettingModal | ✅ | `Admin can create a User policy` | -| Edit user policy → UserResourcePolicySettingModal | ❌ | - | -| Delete user policy → Popconfirm | ✅ | `Admin can delete a User policy` | +| Feature | Status | Test | +|---------|--------|------| +| User policy list rendering | ✅ | `Admin can see User policy list` | +| Create user policy → UserResourcePolicySettingModal | ✅ | `Admin can create a User policy` | +| Edit user policy → UserResourcePolicySettingModal | ❌ | - | +| Delete user policy → Popconfirm | ✅ | `Admin can delete a User policy` | #### Project Policies Tab **Primary action:** "+" → `ProjectResourcePolicySettingModal` **Row actions:** Edit → `ProjectResourcePolicySettingModal`, Delete → Popconfirm -| Feature | Status | Test | -| --------------------------------------------------------- | ------ | ----------------------------------- | -| Project policy list rendering | ✅ | `Admin can see Project policy list` | -| Create project policy → ProjectResourcePolicySettingModal | ✅ | `Admin can create a Project policy` | -| Edit project policy → ProjectResourcePolicySettingModal | ❌ | - | -| Delete project policy → Popconfirm | ✅ | `Admin can delete a Project policy` | +| Feature | Status | Test | +|---------|--------|------| +| Project policy list rendering | ✅ | `Admin can see Project policy list` | +| Create project policy → ProjectResourcePolicySettingModal | ✅ | `Admin can create a Project policy` | +| Edit project policy → ProjectResourcePolicySettingModal | ❌ | - | +| Delete project policy → Popconfirm | ✅ | `Admin can delete a Project policy` | **Coverage: 🔶 10/13 features** @@ -650,22 +654,22 @@ **Row actions:** Edit → `UserSettingModal`, Delete → Popconfirm **Bulk actions:** Bulk edit → `UpdateUsersModal`, Bulk delete → `PurgeUsersModal` -| Feature | Status | Test | -| --------------------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------- | -| Create user → UserSettingModal | ✅ | `Admin can create a new user` | -| Bulk create users → UserSettingModal | ✅ | `Admin can bulk create multiple users` | -| Bulk create single user | ✅ | `Admin can bulk create a single user` | -| Bulk create modal open/cancel | ✅ | `Admin can open bulk create modal from dropdown` / `Admin can cancel bulk user creation` | -| Update user → UserSettingModal | ✅ | `Admin can update user information` | -| Deactivate user | ✅ | `Admin can deactivate a user` | -| Reactivate user | ✅ | `Admin can reactivate an inactive user` | -| Purge user → PurgeUsersModal | ✅ | `Admin can deactivate and permanently delete` | -| Deleted user login blocked | ✅ | `Deleted user cannot log in` | -| Allowed IP restriction enforcement (active session) | ✅ | `User can access pages when their current IP is in the allowed list` / `User is denied access after admin revokes their IP` | -| User name click → UserInfoModal | ❌ | - | -| Bulk edit → UpdateUsersModal | ❌ | - | -| User table filtering | ❌ | - | -| User table sorting | ❌ | - | +| Feature | Status | Test | +| ------------------------------- | ------ | --------------------------------------------- | +| Create user → UserSettingModal | ✅ | `Admin can create a new user` | +| Bulk create users → UserSettingModal | ✅ | `Admin can bulk create multiple users` | +| Bulk create single user | ✅ | `Admin can bulk create a single user` | +| Bulk create modal open/cancel | ✅ | `Admin can open bulk create modal from dropdown` / `Admin can cancel bulk user creation` | +| Update user → UserSettingModal | ✅ | `Admin can update user information` | +| Deactivate user | ✅ | `Admin can deactivate a user` | +| Reactivate user | ✅ | `Admin can reactivate an inactive user` | +| Purge user → PurgeUsersModal | ✅ | `Admin can deactivate and permanently delete` | +| Deleted user login blocked | ✅ | `Deleted user cannot log in` | +| Allowed IP restriction enforcement (active session) | ✅ | `User can access pages when their current IP is in the allowed list` / `User is denied access after admin revokes their IP` | +| User name click → UserInfoModal | ❌ | - | +| Bulk edit → UpdateUsersModal | ❌ | - | +| User table filtering | ❌ | - | +| User table sorting | ❌ | - | #### Credentials Tab @@ -673,14 +677,14 @@ **Table link:** Keypair name → `KeypairInfoModal` **Row actions:** Edit → `KeypairSettingModal`, SSH → `SSHKeypairManagementModal`, Delete → Popconfirm -| Feature | Status | Test | -| ---------------------------------------------- | ------ | ----------------------------------------------------- | -| Keypair list rendering | ✅ | `Admin can see Credential list with expected columns` | -| Keypair name click → KeypairInfoModal | ✅ | `Admin can view Keypair info modal` | -| Active/Inactive filter | ✅ | `Admin can see Active/Inactive radio filter` | -| Create keypair → KeypairSettingModal | ❌ | - | -| Edit keypair → KeypairSettingModal | ❌ | - | -| SSH key management → SSHKeypairManagementModal | ❌ | - | +| Feature | Status | Test | +|---------|--------|------| +| Keypair list rendering | ✅ | `Admin can see Credential list with expected columns` | +| Keypair name click → KeypairInfoModal | ✅ | `Admin can view Keypair info modal` | +| Active/Inactive filter | ✅ | `Admin can see Active/Inactive radio filter` | +| Create keypair → KeypairSettingModal | ❌ | - | +| Edit keypair → KeypairSettingModal | ❌ | - | +| SSH key management → SSHKeypairManagementModal | ❌ | - | **Coverage: 🔶 13/20 features** @@ -690,11 +694,11 @@ **Test files:** [`e2e/maintenance/maintenance.spec.ts`](maintenance/maintenance.spec.ts) -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------- | ------ | ------------------------------------ | -| Recalculate usage | ✅ | `click the Recalculate Usage button` | -| Rescan images | ✅ | `click the Rescan Images button` | -| Other maintenance actions | ❌ | - | +| Recalculate usage | ✅ | `click the Recalculate Usage button` | +| Rescan images | ✅ | `click the Rescan Images button` | +| Other maintenance actions | ❌ | - | **Coverage: 🔶 2/3 features** @@ -710,23 +714,23 @@ **Modals:** `MyKeypairManagementModal` (keypair info/management), `SSHKeypairManagementModal`, `ShellScriptEditModal` -| Feature | Status | Test | -| -------------------------------------------------- | ------ | ------------------------------------------- | -| Language selection | ❌ | - | -| Desktop notifications toggle | ❌ | - | -| Compact sidebar toggle | ❌ | - | -| Auto-logout configuration | ❌ | - | -| My keypair management → MyKeypairManagementModal | ✅ | 25 tests in `my-keypair-management.spec.ts` | -| SSH keypair management → SSHKeypairManagementModal | ❌ | - | -| Bootstrap script → ShellScriptEditModal | ❌ | - | -| User config script → ShellScriptEditModal | ❌ | - | -| Experimental features toggle | ❌ | - | +| Feature | Status | Test | +| -------------------------------------------------- | ------ | ---- | +| Language selection | ❌ | - | +| Desktop notifications toggle | ❌ | - | +| Compact sidebar toggle | ❌ | - | +| Auto-logout configuration | ❌ | - | +| My keypair management → MyKeypairManagementModal | ✅ | 25 tests in `my-keypair-management.spec.ts` | +| SSH keypair management → SSHKeypairManagementModal | ❌ | - | +| Bootstrap script → ShellScriptEditModal | ❌ | - | +| User config script → ShellScriptEditModal | ❌ | - | +| Experimental features toggle | ❌ | - | #### Logs Tab -| Feature | Status | Test | +| Feature | Status | Test | | ----------------- | ------ | ---- | -| Error log viewing | ❌ | - | +| Error log viewing | ❌ | - | **Coverage: 🔶 1/10 features** @@ -740,14 +744,14 @@ **Table link:** Project name → `BAIProjectSettingModal` (edit mode) **Bulk action:** "Bulk Edit" → `BAIProjectBulkEditModal` -| Feature | Status | Test | -| -------------------------------------------------- | ------ | -------------------------------------------------- | -| Project list rendering | ✅ | `Admin can see project list with expected columns` | -| Create project → BAIProjectSettingModal | ✅ | `Admin can create a new project` | -| Project name click → BAIProjectSettingModal (edit) | ✅ | `Admin can edit project` | -| Project filtering | ✅ | `Admin can filter projects by name` | -| Bulk edit → BAIProjectBulkEditModal | ❌ | - | -| Delete project | ✅ | `Admin can delete a project` | +| Feature | Status | Test | +|---------|--------|------| +| Project list rendering | ✅ | `Admin can see project list with expected columns` | +| Create project → BAIProjectSettingModal | ✅ | `Admin can create a new project` | +| Project name click → BAIProjectSettingModal (edit) | ✅ | `Admin can edit project` | +| Project filtering | ✅ | `Admin can filter projects by name` | +| Bulk edit → BAIProjectBulkEditModal | ❌ | - | +| Delete project | ✅ | `Admin can delete a project` | **Coverage: 🔶 5/6 features** @@ -759,10 +763,10 @@ **Tabs:** Usage History | User Session History (conditional) -| Feature | Status | Test | -| ------------------------ | ------ | ----------------------------------------------------------- | -| Allocation history tab | ✅ | `Admin can see Statistics page with Allocation History tab` | -| User session history tab | ✅ | `Admin can switch to User Session History tab` | +| Feature | Status | Test | +|---------|--------|------| +| Allocation history tab | ✅ | `Admin can see Statistics page with Allocation History tab` | +| User session history tab | ✅ | `Admin can switch to User Session History tab` | **Coverage: ✅ 2/2 features** @@ -776,14 +780,14 @@ **Resource group selector:** `SharedResourceGroupSelectForCurrentProject` **Table link:** Session name → `SessionDetailAndContainerLogOpenerLegacy` drawer -| Feature | Status | Test | +| Feature | Status | Test | | ----------------------------------------- | ------ | ---- | -| Pending session list rendering | ❌ | - | -| Resource group filtering | ❌ | - | -| Session name click → SessionDetail drawer | ❌ | - | -| Auto-refresh (7s interval) | ❌ | - | -| Pagination and page size | ❌ | - | -| Column visibility settings | ❌ | - | +| Pending session list rendering | ❌ | - | +| Resource group filtering | ❌ | - | +| Session name click → SessionDetail drawer | ❌ | - | +| Auto-refresh (7s interval) | ❌ | - | +| Pagination and page size | ❌ | - | +| Column visibility settings | ❌ | - | **Coverage: ❌ 0/6 features** @@ -793,10 +797,10 @@ **Test files:** [`e2e/information/information.spec.ts`](information/information.spec.ts) -| Feature | Status | Test | -| -------------------------------- | ------ | ---------------------------------------------------- | -| Information page rendering | ✅ | `Admin can see Information page with server details` | -| Server / cluster details display | ✅ | `Admin can see Information page with server details` | +| Feature | Status | Test | +|---------|--------|------| +| Information page rendering | ✅ | `Admin can see Information page with server details` | +| Server / cluster details display | ✅ | `Admin can see Information page with server details` | **Coverage: ✅ 2/2 features** @@ -814,17 +818,17 @@ #### Main Page (`/reservoir`) -| Feature | Status | Test | +| Feature | Status | Test | | -------------------------------------------------------------- | ------ | ---- | -| Artifact list rendering | ❌ | - | -| Mode toggle (Active/Inactive) | ❌ | - | -| Artifact filtering (name, source, registry, type) | ❌ | - | -| Pull from HuggingFace → ScanArtifactModelsFromHuggingFaceModal | ❌ | - | -| Row action: Pull → BAIImportArtifactModal | ❌ | - | -| Row action: Delete → BAIDeactivateArtifactsModal | ❌ | - | -| Row action: Restore → BAIActivateArtifactsModal | ❌ | - | -| Bulk deactivate/activate | ❌ | - | -| Pagination and page size | ❌ | - | +| Artifact list rendering | ❌ | - | +| Mode toggle (Active/Inactive) | ❌ | - | +| Artifact filtering (name, source, registry, type) | ❌ | - | +| Pull from HuggingFace → ScanArtifactModelsFromHuggingFaceModal | ❌ | - | +| Row action: Pull → BAIImportArtifactModal | ❌ | - | +| Row action: Delete → BAIDeactivateArtifactsModal | ❌ | - | +| Row action: Restore → BAIActivateArtifactsModal | ❌ | - | +| Bulk deactivate/activate | ❌ | - | +| Pagination and page size | ❌ | - | #### Detail Page (`/reservoir/:artifactId`) @@ -833,17 +837,17 @@ **Row actions:** Pull → `BAIImportArtifactModal`, Import to Folder → `ImportArtifactRevisionToFolderModal`, Delete → `BAIDeleteArtifactRevisionsModal` **Bulk actions:** Pull selected, Import to folder, Delete selected -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------------------------------------------------ | ------ | ---- | -| Artifact info display | ❌ | - | -| Revision list rendering | ❌ | - | -| Revision filtering (status, version, size) | ❌ | - | -| Pull latest version | ❌ | - | -| Row action: Pull revision → BAIImportArtifactModal | ❌ | - | -| Row action: Import to folder → ImportArtifactRevisionToFolderModal | ❌ | - | -| Row action: Delete revision → BAIDeleteArtifactRevisionsModal | ❌ | - | -| Bulk pull/import/delete selected revisions | ❌ | - | -| Pulling status alert with progress | ❌ | - | +| Artifact info display | ❌ | - | +| Revision list rendering | ❌ | - | +| Revision filtering (status, version, size) | ❌ | - | +| Pull latest version | ❌ | - | +| Row action: Pull revision → BAIImportArtifactModal | ❌ | - | +| Row action: Import to folder → ImportArtifactRevisionToFolderModal | ❌ | - | +| Row action: Delete revision → BAIDeleteArtifactRevisionsModal | ❌ | - | +| Bulk pull/import/delete selected revisions | ❌ | - | +| Pulling status alert with progress | ❌ | - | **Coverage: ❌ 0/18 features** @@ -858,32 +862,32 @@ #### Theme Customization -| Feature | Status | Test | +| Feature | Status | Test | | -------------------------------------------------- | ------ | ---- | -| Primary color picker | ❌ | - | -| Header background color picker | ❌ | - | -| Link / Info / Error / Success / Text color pickers | ❌ | - | -| Individual color reset buttons | ❌ | - | +| Primary color picker | ❌ | - | +| Header background color picker | ❌ | - | +| Link / Info / Error / Success / Text color pickers | ❌ | - | +| Individual color reset buttons | ❌ | - | #### Logo Customization -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------------------------ | ------ | ---- | -| Wide logo size configuration | ❌ | - | -| Collapsed logo size configuration | ❌ | - | -| Light/Dark mode logo upload & preview | ❌ | - | -| Light/Dark collapsed logo upload & preview | ❌ | - | -| Individual logo reset buttons | ❌ | - | +| Wide logo size configuration | ❌ | - | +| Collapsed logo size configuration | ❌ | - | +| Light/Dark mode logo upload & preview | ❌ | - | +| Light/Dark collapsed logo upload & preview | ❌ | - | +| Individual logo reset buttons | ❌ | - | #### General -| Feature | Status | Test | +| Feature | Status | Test | | ------------------------------------------ | ------ | ---- | -| Preview in new window | ❌ | - | -| JSON config editing → ThemeJsonConfigModal | ❌ | - | -| Reset all to defaults | ❌ | - | -| Search/filter settings | ❌ | - | -| Setting persistence across reload | ❌ | - | +| Preview in new window | ❌ | - | +| JSON config editing → ThemeJsonConfigModal | ❌ | - | +| Reset all to defaults | ❌ | - | +| Search/filter settings | ❌ | - | +| Setting persistence across reload | ❌ | - | **Coverage: ❌ 0/14 features** @@ -895,26 +899,26 @@ **Sub-modals:** `SFTPConnectionInfoModal`, `VNCConnectionInfoModal`, `XRDPConnectionInfoModal`, `VSCodeDesktopConnectionModal`, `TensorboardPathModal`, `AppLaunchConfirmationModal`, `TCPConnectionInfoModal` -| Feature | Status | Test | +| Feature | Status | Test | | -------------------------------------------------- | ------ | -------------------------------------------- | -| Open modal from session actions | ✅ | `User can open app launcher modal` | -| Apps grouped by category | ✅ | `User sees apps grouped by category` | -| App icons and titles correct | ✅ | `User sees correct app icons and titles` | -| Close modal | ✅ | `User can close app launcher modal` | -| Launch Terminal (ttyd) | ✅ | `User can launch Console app` | -| Launch Jupyter Notebook | ✅ | `User can launch Jupyter Notebook app` | -| Launch JupyterLab | ✅ | `User can launch JupyterLab app` | -| Launch VS Code (web) | ✅ | `User can launch Visual Studio Code app` | -| SSH/SFTP → SFTPConnectionInfoModal | ✅ | `User sees SFTP connection info modal` | -| VS Code Desktop → VSCodeDesktopConnectionModal | ✅ | `User sees VS Code Desktop connection modal` | -| VNC → VNCConnectionInfoModal | ❌ | - | -| XRDP → XRDPConnectionInfoModal | ❌ | - | -| Tensorboard → TensorboardPathModal | ❌ | - | -| NNI Board / MLflow UI → AppLaunchConfirmationModal | ❌ | - | -| Generic TCP apps → TCPConnectionInfoModal | ❌ | - | -| Pre-open port apps launch | ❌ | - | -| "Open to Public" option with client IPs | ❌ | - | -| "Preferred Port" option | ❌ | - | +| Open modal from session actions | ✅ | `User can open app launcher modal` | +| Apps grouped by category | ✅ | `User sees apps grouped by category` | +| App icons and titles correct | ✅ | `User sees correct app icons and titles` | +| Close modal | ✅ | `User can close app launcher modal` | +| Launch Terminal (ttyd) | ✅ | `User can launch Console app` | +| Launch Jupyter Notebook | ✅ | `User can launch Jupyter Notebook app` | +| Launch JupyterLab | ✅ | `User can launch JupyterLab app` | +| Launch VS Code (web) | ✅ | `User can launch Visual Studio Code app` | +| SSH/SFTP → SFTPConnectionInfoModal | ✅ | `User sees SFTP connection info modal` | +| VS Code Desktop → VSCodeDesktopConnectionModal | ✅ | `User sees VS Code Desktop connection modal` | +| VNC → VNCConnectionInfoModal | ❌ | - | +| XRDP → XRDPConnectionInfoModal | ❌ | - | +| Tensorboard → TensorboardPathModal | ❌ | - | +| NNI Board / MLflow UI → AppLaunchConfirmationModal | ❌ | - | +| Generic TCP apps → TCPConnectionInfoModal | ❌ | - | +| Pre-open port apps launch | ❌ | - | +| "Open to Public" option with client IPs | ❌ | - | +| "Preferred Port" option | ❌ | - | **Coverage: 🔶 10/18 features** @@ -926,14 +930,14 @@ **Drawer:** `ChatHistoryDrawer` -| Feature | Status | Test | -| -------------------------------- | ------ | ----------------------------------------------------------------------------- | -| Chat card interface | ✅ | `User can see the chat page with endpoint and model selectors` | -| Chat history → ChatHistoryDrawer | ✅ | `User can see chat history drawer after sending a message` | -| New chat creation | ✅ | `User can rename a chat session from the page title` | -| Message sending/receiving | ✅ | `User can send a message and receive a streaming response` | -| Provider/model selection | ✅ | `User can select different endpoints in each chat pane` | -| Chat history deletion | ✅ | `User is redirected to a new chat when deleting the currently active session` | +| Feature | Status | Test | +| -------------------------------- | ------ | ---- | +| Chat card interface | ✅ | `User can see the chat page with endpoint and model selectors` | +| Chat history → ChatHistoryDrawer | ✅ | `User can see chat history drawer after sending a message` | +| New chat creation | ✅ | `User can rename a chat session from the page title` | +| Message sending/receiving | ✅ | `User can send a message and receive a streaming response` | +| Provider/model selection | ✅ | `User can select different endpoints in each chat pane` | +| Chat history deletion | ✅ | `User is redirected to a new chat when deleting the currently active session` | **Coverage: ✅ 6/6 features** @@ -945,20 +949,20 @@ **Plugin fixtures:** `test-plugin.js`, `admin-test-plugin.js`, `plugin-a.js`, `plugin-b.js` -| Feature | Status | Test | -| ---------------------------------------------------- | ------ | -------------------------------------------------------------------------------- | -| Admin sees user-permission plugin in sidebar | ✅ | `Admin can see user-permission plugin menu item in sidebar` | -| User sees user-permission plugin in sidebar | ✅ | `User can see user-permission plugin menu item in sidebar` | -| Admin sees admin-permission plugin in Admin Settings | ✅ | `Admin can see admin-permission plugin in Admin Settings panel` | -| No plugin menu without config | ✅ | `Admin cannot see extra plugin menu when plugin.page is not set` | -| No plugin menu when JS returns 404 | ✅ | `Admin cannot see plugin menu when plugin JS file returns 404` | -| Plugin menu click opens new tab | ✅ | `Admin can open external link plugin in new tab` | -| User cannot see admin-permission plugin | ✅ | `User cannot see admin-permission plugin menu item` | -| Blocklisted plugin is hidden | ✅ | `Admin cannot see plugin that is in the blocklist` | -| Non-blocklisted plugin visible alongside blocklist | ✅ | `Admin can see plugin that is not in the blocklist while blocked item is hidden` | -| Multiple plugins visible simultaneously | ✅ | `Admin can see multiple plugin menu items when multiple plugins are configured` | -| Valid plugin visible when sibling fails to load | ✅ | `Admin can see valid plugin when one of multiple plugins fails to load` | -| Plugin state persists after page reload | ✅ | `Admin can see plugin menu item after page reload` | +| Feature | Status | Test | +|---------|--------|------| +| Admin sees user-permission plugin in sidebar | ✅ | `Admin can see user-permission plugin menu item in sidebar` | +| User sees user-permission plugin in sidebar | ✅ | `User can see user-permission plugin menu item in sidebar` | +| Admin sees admin-permission plugin in Admin Settings | ✅ | `Admin can see admin-permission plugin in Admin Settings panel` | +| No plugin menu without config | ✅ | `Admin cannot see extra plugin menu when plugin.page is not set` | +| No plugin menu when JS returns 404 | ✅ | `Admin cannot see plugin menu when plugin JS file returns 404` | +| Plugin menu click opens new tab | ✅ | `Admin can open external link plugin in new tab` | +| User cannot see admin-permission plugin | ✅ | `User cannot see admin-permission plugin menu item` | +| Blocklisted plugin is hidden | ✅ | `Admin cannot see plugin that is in the blocklist` | +| Non-blocklisted plugin visible alongside blocklist | ✅ | `Admin can see plugin that is not in the blocklist while blocked item is hidden` | +| Multiple plugins visible simultaneously | ✅ | `Admin can see multiple plugin menu items when multiple plugins are configured` | +| Valid plugin visible when sibling fails to load | ✅ | `Admin can see valid plugin when one of multiple plugins fails to load` | +| Plugin state persists after page reload | ✅ | `Admin can see plugin menu item after page reload` | **Coverage: ✅ 12/12 features** @@ -968,30 +972,30 @@ **Test files:** [`e2e/rbac/rbac-role-list.spec.ts`](rbac/rbac-role-list.spec.ts), [`e2e/rbac/rbac-role-crud.spec.ts`](rbac/rbac-role-crud.spec.ts), [`e2e/rbac/rbac-role-detail.spec.ts`](rbac/rbac-role-detail.spec.ts) -| Feature | Status | Test | -| -------------------------------------------------- | ------ | ------------------------------------------------------------------------------- | -| Display RBAC management page with role list table | ✅ | `Superadmin can view the RBAC management page with role list table` | -| Switch between Active/Inactive role filters | ✅ | `Superadmin can switch to Inactive roles filter and back to Active` | -| Search for a role by name using property filter | ✅ | `Superadmin can search for a role by name using the property filter` | -| Filter roles by Source (SYSTEM or CUSTOM) | 🚧 | `Superadmin can filter roles by Source (SYSTEM or CUSTOM)` | -| Empty state when no roles match search | ✅ | `Superadmin sees empty state message when no roles match the search` | -| Sort role list by Role Name column | ✅ | `Superadmin can sort role list by Role Name column` | -| Refresh role list using refresh button | ✅ | `Superadmin can refresh the role list using the refresh button` | -| Create a new custom role with name and description | ✅ | `Superadmin can create a new custom role with name and description` | -| Edit a custom role name and description via drawer | ✅ | `Superadmin can edit a custom role name and description via drawer` | -| System role edit button absent | ✅ | `Superadmin cannot edit a system role name or description (edit button absent)` | -| Deactivate (soft-delete) an active custom role | ✅ | `Superadmin can delete (soft-delete) an active custom role` | -| Activate (restore) a soft-deleted role | ✅ | `Superadmin can activate (restore) a soft-deleted role` | -| Purge (hard-delete) a soft-deleted role | ✅ | `Superadmin can purge (hard-delete) a soft-deleted role` | -| Open role detail drawer by clicking role name | ✅ | `Superadmin can open the role detail drawer by clicking a role name` | -| Drawer shows Role Assignments and Permissions tabs | ✅ | `Drawer shows "Role Assignments" and "Permissions" tabs` | -| Close role detail drawer | ✅ | `Superadmin can close the role detail drawer` | -| Add a permission to a role | ✅ | `Superadmin can add a permission to a role` | -| Delete a permission from a role | ✅ | `Superadmin can delete a permission from a role` | -| Empty state in Permissions tab | ✅ | `Superadmin sees empty state in Permissions tab when role has no permissions` | -| Assign a user to a role | ✅ | `Superadmin can assign a user to a role` | -| Revoke a user from a role | ✅ | `Superadmin can revoke a single user from a role` | -| Empty state in Role Assignments tab | ✅ | `Superadmin sees empty state in Role Assignments tab when role has no users` | +| Feature | Status | Test | +|---------|--------|------| +| Display RBAC management page with role list table | ✅ | `Superadmin can view the RBAC management page with role list table` | +| Switch between Active/Inactive role filters | ✅ | `Superadmin can switch to Inactive roles filter and back to Active` | +| Search for a role by name using property filter | ✅ | `Superadmin can search for a role by name using the property filter` | +| Filter roles by Source (SYSTEM or CUSTOM) | 🚧 | `Superadmin can filter roles by Source (SYSTEM or CUSTOM)` | +| Empty state when no roles match search | ✅ | `Superadmin sees empty state message when no roles match the search` | +| Sort role list by Role Name column | ✅ | `Superadmin can sort role list by Role Name column` | +| Refresh role list using refresh button | ✅ | `Superadmin can refresh the role list using the refresh button` | +| Create a new custom role with name and description | ✅ | `Superadmin can create a new custom role with name and description` | +| Edit a custom role name and description via drawer | ✅ | `Superadmin can edit a custom role name and description via drawer` | +| System role edit button absent | ✅ | `Superadmin cannot edit a system role name or description (edit button absent)` | +| Deactivate (soft-delete) an active custom role | ✅ | `Superadmin can delete (soft-delete) an active custom role` | +| Activate (restore) a soft-deleted role | ✅ | `Superadmin can activate (restore) a soft-deleted role` | +| Purge (hard-delete) a soft-deleted role | ✅ | `Superadmin can purge (hard-delete) a soft-deleted role` | +| Open role detail drawer by clicking role name | ✅ | `Superadmin can open the role detail drawer by clicking a role name` | +| Drawer shows Role Assignments and Permissions tabs | ✅ | `Drawer shows "Role Assignments" and "Permissions" tabs` | +| Close role detail drawer | ✅ | `Superadmin can close the role detail drawer` | +| Add a permission to a role | ✅ | `Superadmin can add a permission to a role` | +| Delete a permission from a role | ✅ | `Superadmin can delete a permission from a role` | +| Empty state in Permissions tab | ✅ | `Superadmin sees empty state in Permissions tab when role has no permissions` | +| Assign a user to a role | ✅ | `Superadmin can assign a user to a role` | +| Revoke a user from a role | ✅ | `Superadmin can revoke a single user from a role` | +| Empty state in Role Assignments tab | ✅ | `Superadmin sees empty state in Role Assignments tab when role has no users` | **Coverage: 🔶 21/22 features** @@ -1029,33 +1033,33 @@ Visual regression tests exist for most pages but only capture screenshots, not f These are core user workflows that affect the largest number of users. -| # | Page/Feature | Reason | Estimated Complexity | +| # | Page/Feature | Reason | Estimated Complexity | | --- | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -------------------- | -| 1 | **Serving - Create & Manage Model Service** (`/serving`, `/service/start`) | Core revenue feature. Zero coverage. Complete CRUD lifecycle needed. | High | -| 2 | **Session Launcher - Advanced Options** (`/session/start`) | Resource allocation, VFolder mounting, and form validation are critical for correct session behavior. | Medium | +| 1 | **Serving - Create & Manage Model Service** (`/serving`, `/service/start`) | Core revenue feature. Zero coverage. Complete CRUD lifecycle needed. | High | +| 2 | **Session Launcher - Advanced Options** (`/session/start`) | Resource allocation, VFolder mounting, and form validation are critical for correct session behavior. | Medium | ### Priority 2: Important - Admin Features, Data Integrity -| # | Page/Feature | Reason | Estimated Complexity | +| # | Page/Feature | Reason | Estimated Complexity | | --- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------------- | -| 3 | **User Settings Persistence** (`/usersettings`) | 2 tabs, 4 modals. Language, auto-logout, SSH keys, shell scripts must persist correctly. | Low | -| 4 | **VFolder - Filtering, Sorting, Bulk ops** (`/data`) | Data page has good CRUD but table interactions and bulk modals (DeleteVFolderModal, RestoreVFolderModal) untested. | Low | -| 5 | **Credential - Keypairs Tab** (`/credential`) | API access keys (3 uncovered features). Security-critical. | Medium | -| 6 | **Reservoir - Artifact Management** (`/reservoir`) | 18 features across main and detail pages. HuggingFace import, revision management, bulk operations. | High | +| 3 | **User Settings Persistence** (`/usersettings`) | 2 tabs, 4 modals. Language, auto-logout, SSH keys, shell scripts must persist correctly. | Low | +| 4 | **VFolder - Filtering, Sorting, Bulk ops** (`/data`) | Data page has good CRUD but table interactions and bulk modals (DeleteVFolderModal, RestoreVFolderModal) untested. | Low | +| 5 | **Credential - Keypairs Tab** (`/credential`) | API access keys (3 uncovered features). Security-critical. | Medium | +| 6 | **Reservoir - Artifact Management** (`/reservoir`) | 18 features across main and detail pages. HuggingFace import, revision management, bulk operations. | High | ### Priority 3: Nice to Have - Edge Cases, Admin Tools -| # | Page/Feature | Reason | Estimated Complexity | +| # | Page/Feature | Reason | Estimated Complexity | | --- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------------------- | -| 7 | **Endpoint Detail - Auto-scaling & Tokens** (`/serving/:serviceId`) | 14 features with 5 modals. Complex admin feature, but lower user count. | High | -| 8 | **Session - Filtering, Drawer** (`/session`) | Session list already has creation/lifecycle coverage. SessionDetailDrawer is significant. | Low | -| 9 | **Environment - Presets** (`/environment`) | 4 uncovered features for Resource Presets tab. Each with CRUD modals. | Medium | -| 10 | **Resources - Resource Groups** (`/agent`) | 5 uncovered features with 3 modals (create/edit/info). Agent drawer also untested. | Medium | -| 11 | **Model Store** (`/model-store`) | Browse/search models. ModelCardModal for detail. Read-only interface. | Low | -| 12 | **Scheduler** (`/scheduler`) | 6 features. Pending session queue monitoring. Admin tool. | Low | -| 13 | **Branding** (`/branding`) | 14 features. Theme/logo customization. Admin tool. | Medium | -| 14 | **Storage Host Settings** (`/storage-settings/:hostname`) | Niche admin feature. | Low | -| 15 | **Chat** (`/chat/:id?`) | ✅ Covered. Mock-based tests for chat UI, history, multi-pane, sync. | - | +| 7 | **Endpoint Detail - Auto-scaling & Tokens** (`/serving/:serviceId`) | 14 features with 5 modals. Complex admin feature, but lower user count. | High | +| 8 | **Session - Filtering, Drawer** (`/session`) | Session list already has creation/lifecycle coverage. SessionDetailDrawer is significant. | Low | +| 9 | **Environment - Presets** (`/environment`) | 4 uncovered features for Resource Presets tab. Each with CRUD modals. | Medium | +| 10 | **Resources - Resource Groups** (`/agent`) | 5 uncovered features with 3 modals (create/edit/info). Agent drawer also untested. | Medium | +| 11 | **Model Store** (`/model-store`) | Browse/search models. ModelCardModal for detail. Read-only interface. | Low | +| 12 | **Scheduler** (`/scheduler`) | 6 features. Pending session queue monitoring. Admin tool. | Low | +| 13 | **Branding** (`/branding`) | 14 features. Theme/logo customization. Admin tool. | Medium | +| 14 | **Storage Host Settings** (`/storage-settings/:hostname`) | Niche admin feature. | Low | +| 15 | **Chat** (`/chat/:id?`) | ✅ Covered. Mock-based tests for chat UI, history, multi-pane, sync. | - | --- @@ -1063,77 +1067,77 @@ These are core user workflows that affect the largest number of users. ### Existing Page Object Models -| Class | Location | Purpose | +| Class | Location | Purpose | | --------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------ | -| `BasePage` | [`e2e/utils/classes/base/BasePage.ts`](utils/classes/base/BasePage.ts) | Base page class | -| `BaseModal` | [`e2e/utils/classes/base/BaseModal.ts`](utils/classes/base/BaseModal.ts) | Base modal class | -| `StartPage` | [`e2e/utils/classes/common/StartPage.ts`](utils/classes/common/StartPage.ts) | Start page helpers | -| `SessionLauncher` | [`e2e/utils/classes/session/SessionLauncher.ts`](utils/classes/session/SessionLauncher.ts) | Session creation helpers | -| `SessionDetailPage` | [`e2e/utils/classes/session/SessionDetailPage.ts`](utils/classes/session/SessionDetailPage.ts) | Session detail helpers | -| `AppLauncherModal` | [`e2e/utils/classes/session/AppLauncherModal.ts`](utils/classes/session/AppLauncherModal.ts) | App launcher helpers | -| `FolderCreationModal` | [`e2e/utils/classes/vfolder/FolderCreationModal.ts`](utils/classes/vfolder/FolderCreationModal.ts) | Folder creation helpers | -| `FolderExplorerModal` | [`e2e/utils/classes/vfolder/FolderExplorerModal.ts`](utils/classes/vfolder/FolderExplorerModal.ts) | Folder explorer helpers | -| `UserSettingModal` | [`e2e/utils/classes/user/UserSettingModal.ts`](utils/classes/user/UserSettingModal.ts) | User settings helpers | -| `PurgeUsersModal` | [`e2e/utils/classes/user/PurgeUsersModal.ts`](utils/classes/user/PurgeUsersModal.ts) | User deletion helpers | -| `NotificationHandler` | [`e2e/utils/classes/common/NotificationHandler.ts`](utils/classes/common/NotificationHandler.ts) | Notification assertion helpers | +| `BasePage` | [`e2e/utils/classes/base/BasePage.ts`](utils/classes/base/BasePage.ts) | Base page class | +| `BaseModal` | [`e2e/utils/classes/base/BaseModal.ts`](utils/classes/base/BaseModal.ts) | Base modal class | +| `StartPage` | [`e2e/utils/classes/common/StartPage.ts`](utils/classes/common/StartPage.ts) | Start page helpers | +| `SessionLauncher` | [`e2e/utils/classes/session/SessionLauncher.ts`](utils/classes/session/SessionLauncher.ts) | Session creation helpers | +| `SessionDetailPage` | [`e2e/utils/classes/session/SessionDetailPage.ts`](utils/classes/session/SessionDetailPage.ts) | Session detail helpers | +| `AppLauncherModal` | [`e2e/utils/classes/session/AppLauncherModal.ts`](utils/classes/session/AppLauncherModal.ts) | App launcher helpers | +| `FolderCreationModal` | [`e2e/utils/classes/vfolder/FolderCreationModal.ts`](utils/classes/vfolder/FolderCreationModal.ts) | Folder creation helpers | +| `FolderExplorerModal` | [`e2e/utils/classes/vfolder/FolderExplorerModal.ts`](utils/classes/vfolder/FolderExplorerModal.ts) | Folder explorer helpers | +| `UserSettingModal` | [`e2e/utils/classes/user/UserSettingModal.ts`](utils/classes/user/UserSettingModal.ts) | User settings helpers | +| `PurgeUsersModal` | [`e2e/utils/classes/user/PurgeUsersModal.ts`](utils/classes/user/PurgeUsersModal.ts) | User deletion helpers | +| `NotificationHandler` | [`e2e/utils/classes/common/NotificationHandler.ts`](utils/classes/common/NotificationHandler.ts) | Notification assertion helpers | ### Shared Utilities -| Utility | Location | Purpose | -| ------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------- | -| `test-util.ts` | [`e2e/utils/test-util.ts`](utils/test-util.ts) | Login, config modification, TOML helpers | -| `test-util-antd.ts` | [`e2e/utils/test-util-antd.ts`](utils/test-util-antd.ts) | Ant Design component interaction helpers | -| `SessionAPIHelper` | [`e2e/utils/classes/session/SessionAPIHelper.ts`](utils/classes/session/SessionAPIHelper.ts) | Create and manage sessions via Backend.AI API | +| Utility | Location | Purpose | +| ------------------- | -------------------------------------------------------- | ---------------------------------------- | +| `test-util.ts` | [`e2e/utils/test-util.ts`](utils/test-util.ts) | Login, config modification, TOML helpers | +| `test-util-antd.ts` | [`e2e/utils/test-util-antd.ts`](utils/test-util-antd.ts) | Ant Design component interaction helpers | +| `SessionAPIHelper` | [`e2e/utils/classes/session/SessionAPIHelper.ts`](utils/classes/session/SessionAPIHelper.ts) | Create and manage sessions via Backend.AI API | ### Page Object Models Needed To efficiently build new E2E tests, these POMs should be created: -| POM | For Page | Priority | +| POM | For Page | Priority | | --------------------- | --------------------- | -------- | -| `ServingPage` | `/serving` | P1 | -| `ServiceLauncherPage` | `/service/start` | P1 | -| `EndpointDetailPage` | `/serving/:serviceId` | P3 | -| `ResourcePolicyPage` | `/resource-policy` | - | -| `UserSettingsPage` | `/usersettings` | P2 | +| `ServingPage` | `/serving` | P1 | +| `ServiceLauncherPage` | `/service/start` | P1 | +| `EndpointDetailPage` | `/serving/:serviceId` | P3 | +| `ResourcePolicyPage` | `/resource-policy` | - | +| `UserSettingsPage` | `/usersettings` | P2 | --- ## Coverage Matrix (Quick Reference) -| Page Route | Functional Tests | Visual Tests | Priority | -| ----------------------------- | :--------------: | :----------: | :------: | -| `/interactive-login` | 🔶 | ✅ | - | -| `/change-password` | ✅ | ❌ | - | -| `/start` | 🔶 | ✅ | - | -| `/dashboard` | 🔶 | ✅ | - | -| `/session` | 🔶 | ✅ | P3 | -| `/session/start` | 🔶 | ✅ | P1 | -| `/serving` | ❌ | ✅ | **P1** | -| `/serving/:serviceId` | 🔶 | ❌ | P3 | -| `/service/start` | ❌ | ❌ | **P1** | -| `/service/update/:endpointId` | ❌ | ❌ | P3 | -| `/data` | 🔶 | ✅ | P2 | -| `/model-store` | ❌ | ❌ | P3 | -| `/storage-settings/:hostname` | ❌ | ❌ | P3 | -| `/my-environment` | ✅ | ✅ | - | -| `/environment` | 🔶 | ✅ | P3 | -| `/settings` (config) | 🔶 | ✅ | - | -| `/agent-summary` | 🔶 | ✅ | P3 | -| `/agent` | 🔶 | ✅ | P3 | -| `/resource-policy` | 🔶 | ✅ | - | -| `/credential` | 🔶 | ✅ | P2 | -| `/maintenance` | 🔶 | ✅ | - | -| `/project` | 🔶 | ❌ | - | -| `/statistics` | ✅ | ❌ | - | -| `/usersettings` | 🔶 | ❌ | **P2** | -| `/scheduler` | ❌ | ❌ | P3 | -| `/information` | ✅ | ✅ | - | -| `/reservoir` | ❌ | ❌ | P2 | -| `/branding` | ❌ | ❌ | P3 | -| `/chat/:id?` | ✅ | ✅ | - | -| App Launcher (modal) | 🔶 | ❌ | - | -| Plugin System (config-based) | ✅ | ❌ | - | +| Page Route | Functional Tests | Visual Tests | Priority | +|------------|:---:|:---:|:---:| +| `/interactive-login` | 🔶 | ✅ | - | +| `/change-password` | ✅ | ❌ | - | +| `/start` | 🔶 | ✅ | - | +| `/dashboard` | 🔶 | ✅ | - | +| `/session` | 🔶 | ✅ | P3 | +| `/session/start` | 🔶 | ✅ | P1 | +| `/serving` | ❌ | ✅ | **P1** | +| `/serving/:serviceId` | 🔶 | ❌ | P3 | +| `/service/start` | ❌ | ❌ | **P1** | +| `/service/update/:endpointId` | ❌ | ❌ | P3 | +| `/data` | 🔶 | ✅ | P2 | +| `/model-store` | ❌ | ❌ | P3 | +| `/storage-settings/:hostname` | ❌ | ❌ | P3 | +| `/my-environment` | ✅ | ✅ | - | +| `/environment` | 🔶 | ✅ | P3 | +| `/settings` (config) | 🔶 | ✅ | - | +| `/agent-summary` | 🔶 | ✅ | P3 | +| `/agent` | 🔶 | ✅ | P3 | +| `/resource-policy` | 🔶 | ✅ | - | +| `/credential` | 🔶 | ✅ | P2 | +| `/maintenance` | 🔶 | ✅ | - | +| `/project` | 🔶 | ❌ | - | +| `/statistics` | ✅ | ❌ | - | +| `/usersettings` | 🔶 | ❌ | **P2** | +| `/scheduler` | ❌ | ❌ | P3 | +| `/information` | ✅ | ✅ | - | +| `/reservoir` | ❌ | ❌ | P2 | +| `/branding` | ❌ | ❌ | P3 | +| `/chat/:id?` | ✅ | ✅ | - | +| App Launcher (modal) | 🔶 | ❌ | - | +| Plugin System (config-based) | ✅ | ❌ | - | --- diff --git a/e2e/admin-model-card/admin-model-card-create.spec.ts b/e2e/admin-model-card/admin-model-card-create.spec.ts index 63bf1556e4..c05db47f04 100644 --- a/e2e/admin-model-card/admin-model-card-create.spec.ts +++ b/e2e/admin-model-card/admin-model-card-create.spec.ts @@ -27,43 +27,75 @@ test.describe( const modal = adminModelCardPage.getCreateModal(); await expect(modal).toBeVisible(); - // Verify key form fields are present + // Verify key form fields are present. + // In antd v6, Form.Item tooltip icons contribute "question-circle" to the accessible + // name, so we locate fields by form item label text rather than exact accessible name. await expect(modal.getByRole('textbox', { name: 'Name' })).toBeVisible(); await expect(modal.getByText('Model Storage Folder')).toBeVisible(); await expect(modal.getByText('Domain').first()).toBeVisible({ timeout: 10000, }); await expect( - modal.getByRole('textbox', { name: 'Author (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Author' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Title (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Model Version (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Version' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Description (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Description' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Task (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Task' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Category (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Category' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'Architecture (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Architecture' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'License (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'License' }) + .getByRole('textbox'), ).toBeVisible(); await expect( - modal.getByRole('textbox', { name: 'README.md (optional)' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'README.md' }) + .getByRole('textbox'), ).toBeVisible(); - // Verify Access Level is present as a required field (no default value) + // Verify Access Level is present as a required field await expect( - modal.getByRole('combobox', { name: 'Access Level' }), + modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select'), ).toBeVisible(); // Verify the Create and Cancel buttons are present in the modal footer @@ -79,6 +111,7 @@ test.describe( test('Superadmin can create a model card with only required fields', async ({ page, }) => { + test.setTimeout(90000); const adminModelCardPage = new AdminModelCardPage(page); const cardName = `e2e-test-required-only-${Date.now()}`; @@ -93,26 +126,35 @@ test.describe( // Fill in the Name field await modal.getByRole('textbox', { name: 'Name' }).fill(cardName); - // Select an available VFolder - await modal.getByRole('combobox').first().click(); + // Select an available VFolder. + // In antd v6 with BAISelect, click .ant-select-content to open the dropdown. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content') + .click(); const vfolderDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); - await expect(vfolderDropdown).toBeVisible(); + await expect(vfolderDropdown).toBeVisible({ timeout: 10000 }); await expect(vfolderDropdown.getByText(/Total \d+ items/)).toBeVisible({ timeout: 10000, }); await vfolderDropdown.locator('.ant-select-item-option').first().click(); - // Select Access Level (required) - await modal.getByRole('combobox', { name: 'Access Level' }).click(); + // Select Access Level (required). Access level options are "Private" (INTERNAL) and "Public". + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select-content') + .click(); const accessDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); await expect(accessDropdown).toBeVisible(); await accessDropdown .locator('.ant-select-item-option') - .filter({ hasText: 'Internal' }) + .filter({ hasText: 'Private' }) .click(); // Click Create @@ -139,6 +181,7 @@ test.describe( test('Superadmin can create a model card with all fields populated', async ({ page, }) => { + test.setTimeout(90000); const adminModelCardPage = new AdminModelCardPage(page); const cardName = `e2e-test-full-card-${Date.now()}`; @@ -153,48 +196,75 @@ test.describe( // Fill Name await modal.getByRole('textbox', { name: 'Name' }).fill(cardName); - // Select VFolder - await modal.getByRole('combobox').first().click(); + // Select VFolder. In antd v6 with BAISelect, click .ant-select-content to open the dropdown. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content') + .click(); const vfolderDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); - await expect(vfolderDropdown).toBeVisible(); + await expect(vfolderDropdown).toBeVisible({ timeout: 10000 }); await expect(vfolderDropdown.getByText(/Total \d+ items/)).toBeVisible({ timeout: 10000, }); await vfolderDropdown.locator('.ant-select-item-option').first().click(); - // Fill optional fields + // Fill optional fields. In antd v6, tooltip icons alter the accessible name so + // we locate textboxes via their parent form item label. await modal - .getByRole('textbox', { name: 'Author (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Author' }) + .getByRole('textbox') .fill('Test Author'); await modal - .getByRole('textbox', { name: 'Title (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox') .fill('Test Model Title'); await modal - .getByRole('textbox', { name: 'Model Version (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Model Version' }) + .getByRole('textbox') .fill('1.0.0'); await modal - .getByRole('textbox', { name: 'Description (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Description' }) + .getByRole('textbox') .fill('This is a test model description'); await modal - .getByRole('textbox', { name: 'Task (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Task' }) + .getByRole('textbox') .fill('text-generation'); await modal - .getByRole('textbox', { name: 'Category (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Category' }) + .getByRole('textbox') .fill('LLM'); await modal - .getByRole('textbox', { name: 'Architecture (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Architecture' }) + .getByRole('textbox') .fill('Transformer'); await modal - .getByRole('textbox', { name: 'License (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'License' }) + .getByRole('textbox') .fill('Apache-2.0'); await modal - .getByRole('textbox', { name: 'README.md (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'README.md' }) + .getByRole('textbox') .fill('# Test Model\nThis is a test model.'); // Change Access Level to Public - await modal.getByRole('combobox', { name: 'Access Level' }).click(); + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select-content') + .click(); await expect( page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') @@ -248,12 +318,17 @@ test.describe( const modal = adminModelCardPage.getCreateModal(); await expect(modal).toBeVisible(); - // Select a VFolder but leave Name empty - await modal.getByRole('combobox').first().click(); + // Select a VFolder but leave Name empty. + // In antd v6 with BAISelect, click .ant-select-content to open the dropdown. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content') + .click(); const vfolderDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); - await expect(vfolderDropdown).toBeVisible(); + await expect(vfolderDropdown).toBeVisible({ timeout: 10000 }); await expect(vfolderDropdown.getByText(/Total \d+ items/)).toBeVisible({ timeout: 10000, }); @@ -283,7 +358,15 @@ test.describe( const modal = adminModelCardPage.getCreateModal(); await expect(modal).toBeVisible(); - // Fill Name but leave VFolder empty + // Fill Name but leave VFolder empty. + // Wait for the VFolder select to load out of Suspense before filling the name, + // so the form field is registered and will fire validation on submit. + await expect( + modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content'), + ).toBeVisible({ timeout: 15000 }); await modal .getByRole('textbox', { name: 'Name' }) .fill('test-no-vfolder'); @@ -292,7 +375,9 @@ test.describe( await adminModelCardPage.getCreateModalSubmitButton().click(); // Verify validation error "VFolder is required." - await expect(modal.getByText('VFolder is required.')).toBeVisible(); + await expect(modal.getByText('VFolder is required.')).toBeVisible({ + timeout: 10000, + }); // Verify the modal remains open await expect(modal).toBeVisible(); diff --git a/e2e/admin-model-card/admin-model-card-delete.spec.ts b/e2e/admin-model-card/admin-model-card-delete.spec.ts index 90317b1e64..fcc293dfc0 100644 --- a/e2e/admin-model-card/admin-model-card-delete.spec.ts +++ b/e2e/admin-model-card/admin-model-card-delete.spec.ts @@ -1,7 +1,12 @@ // spec: e2e/.agent-output/test-plan-admin-model-card.md // section: 5. Delete Model Card import { AdminModelCardPage } from '../utils/classes/AdminModelCardPage'; -import { loginAsAdmin, webuiEndpoint } from '../utils/test-util'; +import { + deleteForeverAndVerifyFromTrash, + loginAsAdmin, + moveToTrashAndVerify, + webuiEndpoint, +} from '../utils/test-util'; import { test, expect } from '@playwright/test'; test.describe( @@ -18,13 +23,19 @@ test.describe( test('Superadmin can delete a model card via the trash icon with confirmation', async ({ page, }) => { + test.setTimeout(90000); const adminModelCardPage = new AdminModelCardPage(page); - const cardName = `e2e-test-delete-single-${Date.now()}`; + const timestamp = Date.now(); + const folderName = `e2e-test-delete-single-folder-${timestamp}`; + const cardName = `e2e-test-delete-single-${timestamp}`; - // Setup: create a model card to delete + // Setup: create a dedicated folder and model card await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); - await adminModelCardPage.createModelCard({ name: cardName }); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); // Navigate back and find the row await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); @@ -41,7 +52,7 @@ test.describe( confirmDialog.getByText(/Are you sure you want to delete/), ).toBeVisible(); await expect( - confirmDialog.getByText(cardName, { exact: true }), + confirmDialog.getByText(cardName, { exact: true }).first(), ).toBeVisible(); await expect( confirmDialog.getByText('This action cannot be undone.'), @@ -51,11 +62,19 @@ test.describe( await expect(adminModelCardPage.getDeleteConfirmButton()).toBeVisible(); await expect(adminModelCardPage.getDeleteCancelButton()).toBeVisible(); - // Click Delete to confirm + // The "Also delete folder" checkbox should be visible (card has an associated folder) + await expect( + adminModelCardPage.getAlsoDeleteFolderCheckbox(), + ).toBeVisible(); + + // Type card name to confirm (requireConfirmInput is set on the single-delete modal) + await adminModelCardPage.getDeleteConfirmInput().fill(cardName); + + // Click Delete to confirm (leave folder checkbox unchecked — folder cleanup handled separately) await adminModelCardPage.getDeleteConfirmButton().click(); // Verify success message - await expect(page.getByText('Model card has been deleted.')).toBeVisible({ + await expect(page.getByText(/Model card has been deleted/)).toBeVisible({ timeout: 15000, }); @@ -63,19 +82,29 @@ test.describe( await expect(adminModelCardPage.getPaginationInfo()).toContainText( '0 items', ); + + // Cleanup: move folder to trash then permanently delete + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); // 5.2 Superadmin can cancel a single-delete confirmation without deleting test('Superadmin can cancel a single-delete confirmation without deleting', async ({ page, }) => { + test.setTimeout(90000); const adminModelCardPage = new AdminModelCardPage(page); - const cardName = `e2e-test-no-delete-${Date.now()}`; + const timestamp = Date.now(); + const folderName = `e2e-test-no-delete-folder-${timestamp}`; + const cardName = `e2e-test-no-delete-${timestamp}`; - // Setup: create a model card to keep + // Setup: create a dedicated folder and model card await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); - await adminModelCardPage.createModelCard({ name: cardName }); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); // Navigate back and filter await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); @@ -94,17 +123,20 @@ test.describe( // Verify the model card is still in the table await expect(adminModelCardPage.getRowByName(cardName)).toBeVisible(); - // Cleanup: delete the test model card + // Cleanup: delete card only, then move folder to trash and permanently delete await adminModelCardPage.deleteModelCardByName(cardName); + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); // 5.3 Superadmin can select multiple model cards and delete them in bulk test('Superadmin can select multiple model cards and delete them in bulk', async ({ page, }) => { - test.setTimeout(90000); + test.setTimeout(180000); const adminModelCardPage = new AdminModelCardPage(page); const timestamp = Date.now(); + const folderName = `e2e-test-bulk-delete-folder-${timestamp}`; const filterPrefix = `e2e-test-bulk-delete-${timestamp}`; const cardNames = [ `${filterPrefix}-1`, @@ -112,16 +144,27 @@ test.describe( `${filterPrefix}-3`, ]; - // Setup: create three model cards + // Setup: create a shared folder via the "+" button for the first card, + // then reuse it for the remaining cards await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); - for (const name of cardNames) { - await adminModelCardPage.createModelCard({ name }); + await adminModelCardPage.createModelCard({ + name: cardNames[0], + createNewFolderName: folderName, + }); + + for (const name of cardNames.slice(1)) { await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name, + vfolderTitle: folderName, + }); } // Filter to show only this run's test cards (timestamp ensures uniqueness) + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); await adminModelCardPage.applyNameFilter(filterPrefix); await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ timeout: 10000, @@ -157,30 +200,45 @@ test.describe( await bulkDialog.getByRole('button', { name: 'Delete' }).click(); // Wait for bulk delete to complete — dialog closes when all mutations finish - await expect(bulkDialog).toBeHidden({ timeout: 45000 }); + await expect(bulkDialog).toBeHidden({ timeout: 90000 }); // Verify the selection label disappears await expect(adminModelCardPage.getSelectionLabel()).toBeHidden(); + + // Cleanup: model cards were deleted but the shared folder remains; + // move it to trash and permanently delete + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); // 5.4 Superadmin can cancel bulk deletion test('Superadmin can cancel bulk deletion', async ({ page }) => { - test.setTimeout(90000); + test.setTimeout(180000); const adminModelCardPage = new AdminModelCardPage(page); const timestamp = Date.now(); + const folderName = `e2e-test-bulk-cancel-folder-${timestamp}`; const filterPrefix = `e2e-test-bulk-cancel-${timestamp}`; const cardNames = [`${filterPrefix}-1`, `${filterPrefix}-2`]; - // Setup: create two model cards + // Setup: create a shared folder via the "+" button for the first card, + // then reuse it for the second card await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); - for (const name of cardNames) { - await adminModelCardPage.createModelCard({ name }); - await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); - await adminModelCardPage.waitForTableLoad(); - } + await adminModelCardPage.createModelCard({ + name: cardNames[0], + createNewFolderName: folderName, + }); + + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name: cardNames[1], + vfolderTitle: folderName, + }); // Filter to show only this run's test cards (timestamp ensures uniqueness) + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); await adminModelCardPage.applyNameFilter(filterPrefix); await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ timeout: 10000, @@ -210,20 +268,39 @@ test.describe( await expect(adminModelCardPage.getRowByName(name)).toBeVisible(); } - // Cleanup: delete the test model cards + // Cleanup: delete each model card (card only), then clean up the shared folder for (const name of cardNames) { await adminModelCardPage.deleteModelCardByName(name); } + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); // 5.5 Superadmin can clear selection using the BAISelectionLabel clear button test('Superadmin can clear selection using the BAISelectionLabel clear button', async ({ page, }) => { + test.setTimeout(90000); const adminModelCardPage = new AdminModelCardPage(page); + const timestamp = Date.now(); + const folderName = `e2e-test-clear-sel-folder-${timestamp}`; + const cardName = `e2e-test-clear-sel-${timestamp}`; + // Setup: create a model card so the table has at least one row with a checkbox await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); + + // Navigate back and filter to the created card to ensure it is visible + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 10000, + }); // Check the checkbox for the first row const firstRowCheckbox = adminModelCardPage @@ -243,28 +320,205 @@ test.describe( // Verify the BAISelectionLabel disappears await expect(adminModelCardPage.getSelectionLabel()).toBeHidden(); + + // Cleanup: delete the model card then clean up the folder + await adminModelCardPage.deleteModelCardByName(cardName); + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); // 5.6 Superadmin can select all model cards using the header checkbox test('Superadmin can select all model cards on the current page using the header checkbox', async ({ page, }) => { + test.setTimeout(150000); const adminModelCardPage = new AdminModelCardPage(page); + const timestamp = Date.now(); + const folderName = `e2e-test-select-all-folder-${timestamp}`; + const cardName = `e2e-test-select-all-${timestamp}`; + // Setup: create a model card so the table has at least one row await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); + + // Navigate back and filter to ensure the created card is visible + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); + // Wait for the specific filtered card to appear, confirming the filter has been applied + await expect(adminModelCardPage.getRowByName(cardName)).toBeVisible({ + timeout: 15000, + }); // Click the "select all" checkbox in the table header await adminModelCardPage.getHeaderCheckbox().check(); - // Verify the selection label appears + // Verify the selection label appears and shows at least 1 item selected. + // Note: antd table "select all" may select all backend records (not just the + // filtered/visible rows on the current page), so we assert a positive count + // rather than comparing to the visible row count. await expect(adminModelCardPage.getSelectionLabel()).toBeVisible(); + const selectionText = await adminModelCardPage + .getSelectionLabel() + .textContent(); + expect(selectionText).toMatch(/\d+ selected/); + const selectedCount = parseInt( + selectionText?.match(/(\d+) selected/)?.[1] ?? '0', + ); + expect(selectedCount).toBeGreaterThan(0); + + // Cleanup: navigate fresh to reset selection state, then delete the model card + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); + await expect(adminModelCardPage.getRowByName(cardName)).toBeVisible({ + timeout: 15000, + }); + await adminModelCardPage.deleteModelCardByName(cardName); + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); + }); + + // 5.7 Superadmin can delete a model card and its associated folder together + test('Superadmin can delete a model card and its associated folder, and navigate to trash with folder filter', async ({ + page, + }) => { + test.setTimeout(90000); + const adminModelCardPage = new AdminModelCardPage(page); + const timestamp = Date.now(); + const folderName = `e2e-test-delete-folder-${timestamp}`; + const cardName = `e2e-test-delete-with-folder-${timestamp}`; + + // Create a model card with a new dedicated folder via the "+" button + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); + + // Navigate back and filter for the created card + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); + // Wait for the filtered row to appear before clicking delete + await expect(adminModelCardPage.getRowByName(cardName)).toBeVisible({ + timeout: 15000, + }); + + // Open the delete confirmation dialog + await adminModelCardPage.clickDeleteForRow(cardName); + + // Verify the "Also delete the associated model folder" checkbox is visible + await expect( + adminModelCardPage.getAlsoDeleteFolderCheckbox(), + ).toBeVisible(); + + // Verify the folder link shows the expected folder name + const folderLink = adminModelCardPage.getFolderNameLinkInDeleteDialog(); + await expect(folderLink).toBeVisible(); + await expect(folderLink).toHaveText(folderName); + + // Check the "Also delete the associated model folder" checkbox + await adminModelCardPage.getAlsoDeleteFolderCheckbox().check(); + await expect( + adminModelCardPage.getAlsoDeleteFolderCheckbox(), + ).toBeChecked(); + + // Type card name to confirm (requireConfirmInput is set on the single-delete modal) + await adminModelCardPage.getDeleteConfirmInput().fill(cardName); + + // Confirm deletion + await adminModelCardPage.getDeleteConfirmButton().click(); - // Verify the selection count matches the number of rows on the page - const rowCount = await adminModelCardPage.getDataRows().count(); - await expect(adminModelCardPage.getSelectionLabel()).toContainText( - `${rowCount} selected`, + // Verify the success notification for card + folder deletion + await expect( + page.getByText('Model card and folder have been moved to trash.'), + ).toBeVisible({ timeout: 30000 }); + + // Verify "Go to Data > Trash" link is visible in the notification + const goToTrashLink = page.getByText('Go to Data > Trash'); + await expect(goToTrashLink).toBeVisible(); + + // Click "Go to Data > Trash" and verify URL includes folder filter + await goToTrashLink.click(); + await page.waitForURL( + (url) => + url.pathname === '/data' && + url.searchParams.get('statusCategory') === 'deleted' && + url.searchParams.get('filter') === `name == "${folderName}"`, + { timeout: 10000 }, ); + + // Verify the folder row is visible in the trash list and permanently delete it + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); + }); + + // 5.8 Superadmin deletes card only: notification shows correct message and Go to Trash navigates correctly + test('Superadmin can delete a model card only and navigate to trash without folder filter', async ({ + page, + }) => { + test.setTimeout(90000); + const adminModelCardPage = new AdminModelCardPage(page); + const timestamp = Date.now(); + const folderName = `e2e-test-keep-folder-${timestamp}`; + const cardName = `e2e-test-delete-card-only-${timestamp}`; + + // Create a model card with a new dedicated folder via the "+" button + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ + name: cardName, + createNewFolderName: folderName, + }); + + // Navigate back and filter + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); + + // Open delete dialog + await adminModelCardPage.clickDeleteForRow(cardName); + + // Leave "Also delete folder" checkbox unchecked (default) + await expect( + adminModelCardPage.getAlsoDeleteFolderCheckbox(), + ).not.toBeChecked(); + + // Type card name to confirm (requireConfirmInput is set on the single-delete modal) + await adminModelCardPage.getDeleteConfirmInput().fill(cardName); + + // Confirm deletion + await adminModelCardPage.getDeleteConfirmButton().click(); + + // Verify the notification message for card-only deletion + await expect( + page.getByText( + 'Model card has been deleted. The model folder was not deleted.', + ), + ).toBeVisible({ timeout: 15000 }); + + // Verify "Go to Data > Trash" link is visible + const goToTrashLink = page.getByText('Go to Data > Trash'); + await expect(goToTrashLink).toBeVisible(); + + // Click "Go to Data > Trash" and verify URL (no folder filter) + await goToTrashLink.click(); + await page.waitForURL( + (url) => + url.pathname === '/data' && + url.searchParams.get('statusCategory') === 'deleted' && + !url.searchParams.has('filter'), + { timeout: 10000 }, + ); + + // Cleanup: move the kept test folder to trash then permanently delete it + await moveToTrashAndVerify(page, folderName, 'admin-data'); + await deleteForeverAndVerifyFromTrash(page, folderName, 'admin-data'); }); }, ); diff --git a/e2e/admin-model-card/admin-model-card-edit.spec.ts b/e2e/admin-model-card/admin-model-card-edit.spec.ts index b45d6bbe1d..83ebe0bc5a 100644 --- a/e2e/admin-model-card/admin-model-card-edit.spec.ts +++ b/e2e/admin-model-card/admin-model-card-edit.spec.ts @@ -26,30 +26,43 @@ test.describe( await expect(modal).toBeVisible(); await modal.getByRole('textbox', { name: 'Name' }).fill(testCardName); - await modal.getByRole('combobox').first().click(); + // In antd v6 with BAISelect, click .ant-select-content to open the dropdown reliably. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content') + .click(); const vfolderDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); - await expect(vfolderDropdown).toBeVisible(); + await expect(vfolderDropdown).toBeVisible({ timeout: 10000 }); await expect(vfolderDropdown.getByText(/Total \d+ items/)).toBeVisible({ timeout: 10000, }); await vfolderDropdown.locator('.ant-select-item-option').first().click(); await expect(vfolderDropdown).toBeHidden(); - // Select Access Level (required field) - await modal.getByRole('combobox', { name: 'Access Level' }).click(); + // Select Access Level (required field). Access level options are "Private" (INTERNAL) and "Public". + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select-content') + .click(); const accessDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); await expect(accessDropdown).toBeVisible(); await accessDropdown .locator('.ant-select-item-option') - .filter({ hasText: 'Internal' }) + .filter({ hasText: 'Private' }) .click(); + // In antd v6, Form.Item tooltip icons contribute to the accessible name. + // Use the form item container to locate the textbox by label text. await modal - .getByRole('textbox', { name: 'Title (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox') .fill('Original Title'); await adminModelCardPage.getCreateModalSubmitButton().click(); await expect(page.getByText('Model card has been created.')).toBeVisible({ @@ -140,15 +153,21 @@ test.describe( await adminModelCardPage.openEditModal(testCardName); const modal = adminModelCardPage.getEditModal(); - // Clear the Title field and type a new value - const titleInput = modal.getByRole('textbox', { - name: 'Title (optional)', - }); + // Clear the Title field and type a new value. + // In antd v6, tooltip icons alter the accessible name — use form item container. + const titleInput = modal + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox'); await titleInput.clear(); await titleInput.fill('Updated Title'); - // Change Access Level to Public - await modal.getByRole('combobox', { name: 'Access Level' }).click(); + // Change Access Level to Public. In antd v6, use .ant-select-content to open dropdown. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select-content') + .click(); const accessDropdown = page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') .first(); @@ -173,10 +192,10 @@ test.describe( const updatedRow = adminModelCardPage.getRowByName(testCardName); await expect( updatedRow.getByRole('cell', { name: 'Updated Title' }), - ).toBeVisible(); + ).toBeVisible({ timeout: 15000 }); await expect( updatedRow.getByRole('cell', { name: 'Public' }), - ).toBeVisible(); + ).toBeVisible({ timeout: 15000 }); }); // 4.3 Superadmin cannot save an edit when the Name field is cleared @@ -241,10 +260,12 @@ test.describe( await adminModelCardPage.openEditModal(testCardName); const modal = adminModelCardPage.getEditModal(); - // Change the Title to a value that should not be saved - const titleInput = modal.getByRole('textbox', { - name: 'Title (optional)', - }); + // Change the Title to a value that should not be saved. + // In antd v6, tooltip icons alter the accessible name — use form item container. + const titleInput = modal + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox'); await titleInput.clear(); await titleInput.fill('Should Not Save'); diff --git a/e2e/admin-model-card/admin-model-card-page-load.spec.ts b/e2e/admin-model-card/admin-model-card-page-load.spec.ts index b84b409f1d..2ac25d5dcb 100644 --- a/e2e/admin-model-card/admin-model-card-page-load.spec.ts +++ b/e2e/admin-model-card/admin-model-card-page-load.spec.ts @@ -24,7 +24,7 @@ test.describe( // Verify the "Model Store Management" tab is active await expect( page.getByText('Model Store Management').first(), - ).toBeVisible(); + ).toBeVisible({ timeout: 15000 }); // Verify the "Create Model Card" button is visible await expect(adminModelCardPage.getCreateModelCardButton()).toBeVisible(); @@ -60,18 +60,26 @@ test.describe( }); // 1.2 Superadmin can see model card rows with correct data in the table + // This test creates its own model card to guarantee data exists in the table. test('Superadmin can see model card rows with correct data in the table', async ({ page, - }) => { + }, testInfo) => { const adminModelCardPage = new AdminModelCardPage(page); + const cardName = `e2e-test-pageload-${testInfo.workerIndex}-${Date.now()}`; + + // Create a dedicated model card so the table is guaranteed to have data + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ name: cardName }); - // Navigate and wait for at least one row + // Navigate back and filter to the created card await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(cardName); - // Wait for at least one data row to appear - const firstRow = adminModelCardPage.getDataRows().first(); - await expect(firstRow).toBeVisible(); + // Wait for the filtered row to appear + const firstRow = adminModelCardPage.getRowByName(cardName); + await expect(firstRow).toBeVisible({ timeout: 15000 }); // Verify the name cell has setting and trash bin buttons await expect( @@ -81,15 +89,19 @@ test.describe( firstRow.getByRole('button', { name: 'trash bin' }), ).toBeVisible(); - // Verify Access Level cell shows a tag (Public or Internal) + // Verify Access Level cell shows a tag (Public, Private, or Internal) + // "Private" is the display label for the INTERNAL access level in the UI const accessLevelCell = firstRow.getByRole('cell', { - name: /Public|Internal/, + name: /Public|Private|Internal/, }); await expect(accessLevelCell).toBeVisible(); // Verify Created At cell shows a date in YYYY-MM-DD HH:mm format const createdAtCell = firstRow.locator('td').last(); await expect(createdAtCell).toHaveText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/); + + // Cleanup: delete the created model card + await adminModelCardPage.deleteModelCardByName(cardName); }); // 1.3 Superadmin can see pagination controls diff --git a/e2e/admin-model-card/admin-model-card-sort-refresh.spec.ts b/e2e/admin-model-card/admin-model-card-sort-refresh.spec.ts index 180f1490f4..5040efae23 100644 --- a/e2e/admin-model-card/admin-model-card-sort-refresh.spec.ts +++ b/e2e/admin-model-card/admin-model-card-sort-refresh.spec.ts @@ -8,8 +8,32 @@ test.describe( 'Admin Model Card Management - Refresh and Sorting', { tag: ['@admin-model-card', '@admin', '@functional'] }, () => { - test.beforeEach(async ({ page, request }) => { + let testCardName: string; + + test.beforeEach(async ({ page, request }, testInfo) => { + testCardName = `e2e-test-sort-${testInfo.workerIndex}-${Date.now()}`; await loginAsAdmin(page, request); + + // Create a test model card so the table is guaranteed to have data + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + const adminModelCardPage = new AdminModelCardPage(page); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ name: testCardName }); + }); + + test.afterEach(async ({ page }) => { + try { + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + const adminModelCardPage = new AdminModelCardPage(page); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(testCardName); + const row = adminModelCardPage.getRowByName(testCardName); + if ((await row.count()) > 0) { + await adminModelCardPage.deleteModelCardByName(testCardName); + } + } catch { + // Ignore cleanup errors + } }); // 6.1 Superadmin can refresh the table using the fetch key button @@ -25,7 +49,9 @@ test.describe( // Verify the table reloads (wait for table to still be visible after refresh) await adminModelCardPage.waitForTableLoad(); - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); }); // 7.1 Superadmin can sort model cards by Name in ascending order @@ -43,7 +69,9 @@ test.describe( await expect(page).toHaveURL(/order=name/); // Verify rows are reordered (at least the table is still showing) - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); // Verify the sort indicator shows ascending const nameHeader = page.getByRole('columnheader', { name: 'Name' }); @@ -67,7 +95,9 @@ test.describe( await expect(page).toHaveURL(/order=-name/); // Verify rows are still displayed - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); }); // 7.3 Superadmin can sort model cards by Created At @@ -90,7 +120,9 @@ test.describe( await expect(page).toHaveURL(/order=-createdAt/); // Verify rows are still displayed - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); }); // 7.4 Superadmin can switch sort from one column to another @@ -111,7 +143,9 @@ test.describe( await expect(page).not.toHaveURL(/order=name/); // Verify rows are still displayed - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); }); }, ); diff --git a/e2e/admin-model-card/admin-model-card-url-state.spec.ts b/e2e/admin-model-card/admin-model-card-url-state.spec.ts index 7988cb05e3..806a091bd2 100644 --- a/e2e/admin-model-card/admin-model-card-url-state.spec.ts +++ b/e2e/admin-model-card/admin-model-card-url-state.spec.ts @@ -8,8 +8,32 @@ test.describe( 'Admin Model Card Management - URL State Persistence', { tag: ['@admin-model-card', '@admin', '@functional'] }, () => { - test.beforeEach(async ({ page, request }) => { + let testCardName: string; + + test.beforeEach(async ({ page, request }, testInfo) => { + testCardName = `e2e-test-url-${testInfo.workerIndex}-${Date.now()}`; await loginAsAdmin(page, request); + + // Create a test model card so the table is guaranteed to have data + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + const adminModelCardPage = new AdminModelCardPage(page); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.createModelCard({ name: testCardName }); + }); + + test.afterEach(async ({ page }) => { + try { + await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); + const adminModelCardPage = new AdminModelCardPage(page); + await adminModelCardPage.waitForTableLoad(); + await adminModelCardPage.applyNameFilter(testCardName); + const row = adminModelCardPage.getRowByName(testCardName); + if ((await row.count()) > 0) { + await adminModelCardPage.deleteModelCardByName(testCardName); + } + } catch { + // Ignore cleanup errors + } }); // 10.1 Filter state is persisted in the URL query parameters @@ -20,6 +44,10 @@ test.describe( await page.goto(`${webuiEndpoint}/admin-serving?tab=model-store`); await adminModelCardPage.waitForTableLoad(); + // Wait for at least one data row to be visible before reading its name + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); // Get the name of the first model card to use as a filter value const firstRowName = await adminModelCardPage .getDataRows() @@ -80,7 +108,9 @@ test.describe( await expect(page).toHaveURL(/order=name/); // Verify rows are still displayed (sort preserved) - await expect(adminModelCardPage.getDataRows().first()).toBeVisible(); + await expect(adminModelCardPage.getDataRows().first()).toBeVisible({ + timeout: 15000, + }); }); // 10.3 Pagination state is persisted in the URL query parameters diff --git a/e2e/utils/classes/AdminModelCardPage.ts b/e2e/utils/classes/AdminModelCardPage.ts index c00073518c..71632acf8f 100644 --- a/e2e/utils/classes/AdminModelCardPage.ts +++ b/e2e/utils/classes/AdminModelCardPage.ts @@ -174,9 +174,78 @@ export class AdminModelCardPage { .last(); } + getFolderCreateDialog(): Locator { + return this.page.getByRole('dialog', { + name: 'Create a new storage folder', + }); + } + + async createNewFolderViaPlus(folderName: string): Promise { + const modal = this.getCreateModal(); + // The "+" button is next to the Model Storage Folder select. + // It has no accessible name (icon-only button with PlusIcon from lucide-react), + // so we locate it by finding the button within the "Model Storage Folder" form item. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .getByRole('button') + .click(); + + // After clicking "+", either: + // (a) a Popconfirm appears asking to "Change Project" first, or + // (b) the FolderCreateModal opens directly (project is already model-store). + // Wait for whichever appears first so the direct-open path doesn't always pay + // the full Popconfirm timeout. If the Popconfirm branch appears, click it and + // then continue waiting for the folder dialog. + const folderDialog = this.getFolderCreateDialog(); + const changeProjectButton = this.page.getByRole('button', { + name: 'Change Project', + }); + + await expect(changeProjectButton.or(folderDialog)).toBeVisible({ + timeout: 5000, + }); + + if (await changeProjectButton.isVisible()) { + await changeProjectButton.click(); + } + + await expect(folderDialog).toBeVisible({ timeout: 15000 }); + + // initialValidate={true} calls validateFields() in afterOpenChange, which triggers + // a re-render. Soft wait for the "required" error — it's not guaranteed to appear + // before we fill, so we don't fail the test if it's absent. + await expect(folderDialog.getByText('Folder name is required')) + .toBeVisible({ timeout: 5000 }) + .catch(() => {}); + await folderDialog + .locator('.ant-form-item') + .filter({ hasText: 'Folder name' }) + .getByRole('textbox') + .fill(folderName); + await folderDialog + .getByRole('button', { name: 'Create', exact: true }) + .click(); + await expect(folderDialog).toBeHidden({ timeout: 15000 }); + + // onRequestClose asynchronously sets `vfolderId` in the Create Model Card form and + // triggers a BAIVFolderSelect refetch. Assert the VFolder select reflects the + // newly created folder name before proceeding so downstream submit steps don't + // race the refetch. + // In antd v6 with BAISelect, the selected value text is rendered directly inside + // .ant-select-content (which gains .ant-select-content-has-value when a value is set). + await expect( + modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }) + .locator('.ant-select-content'), + ).toContainText(folderName, { timeout: 15000 }); + } + async fillCreateModal(fields: { name: string; vfolderTitle?: string; + createNewFolderName?: string; author?: string; title?: string; modelVersion?: string; @@ -186,90 +255,121 @@ export class AdminModelCardPage { architecture?: string; license?: string; readme?: string; - accessLevel?: 'Public' | 'Internal'; + accessLevel?: 'Public' | 'Private'; }): Promise { const modal = this.getCreateModal(); await expect(modal).toBeVisible(); await modal.getByRole('textbox', { name: 'Name' }).fill(fields.name); - // Always select a VFolder: use specified title or pick the first available option - await modal.getByRole('combobox').first().click(); - // Wait for the VFolder query to load options (BAIVFolderSelect uses network-only fetch on open) - const dropdown = this.page - .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') - .first(); - await expect(dropdown).toBeVisible(); - // Wait for the "Total N items" footer to appear, indicating options have loaded - await expect(dropdown.getByText(/Total \d+ items/)).toBeVisible({ - timeout: 10000, - }); - if (fields.vfolderTitle) { - await dropdown.getByTitle(fields.vfolderTitle).click(); + if (fields.createNewFolderName) { + // Create a new folder via the "+" button — it will be auto-selected after creation + await this.createNewFolderViaPlus(fields.createNewFolderName); } else { - await dropdown.locator('.ant-select-item-option').first().click(); + // Select an existing VFolder: use specified title or pick the first available option. + // In antd v6 with BAISelect, clicking the .ant-select-content container reliably + // opens the dropdown (clicking the raw combobox input does not open it). + const vfolderFormItem = modal + .locator('.ant-form-item') + .filter({ hasText: 'Model Storage Folder' }); + await vfolderFormItem.locator('.ant-select-content').click(); + // Wait for the VFolder query to load options (BAIVFolderSelect uses network-only fetch on open) + const dropdown = this.page + .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') + .first(); + await expect(dropdown).toBeVisible({ timeout: 10000 }); + // Wait for the "Total N items" footer to appear, indicating options have loaded + await expect(dropdown.getByText(/Total \d+ items/)).toBeVisible({ + timeout: 10000, + }); + if (fields.vfolderTitle) { + await dropdown.getByTitle(fields.vfolderTitle).click(); + } else { + await dropdown.locator('.ant-select-item-option').first().click(); + } + // Wait for VFolder dropdown to fully close before interacting with other fields + await expect(dropdown).toBeHidden(); } - // Wait for VFolder dropdown to fully close before interacting with other fields - await expect(dropdown).toBeHidden(); if (fields.author) { + // In antd v6, Form.Item tooltip icons contribute to the accessible name. + // Use the form item container to locate the textbox by label text instead. await modal - .getByRole('textbox', { name: 'Author (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Author' }) + .getByRole('textbox') .fill(fields.author); } if (fields.title) { await modal - .getByRole('textbox', { name: 'Title (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Title' }) + .getByRole('textbox') .fill(fields.title); } if (fields.modelVersion) { await modal - .getByRole('textbox', { name: 'Model Version (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Model Version' }) + .getByRole('textbox') .fill(fields.modelVersion); } if (fields.description) { await modal - .getByRole('textbox', { name: 'Description (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Description' }) + .getByRole('textbox') .fill(fields.description); } if (fields.task) { await modal - .getByRole('textbox', { name: 'Task (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Task' }) + .getByRole('textbox') .fill(fields.task); } if (fields.category) { await modal - .getByRole('textbox', { name: 'Category (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Category' }) + .getByRole('textbox') .fill(fields.category); } if (fields.architecture) { await modal - .getByRole('textbox', { name: 'Architecture (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'Architecture' }) + .getByRole('textbox') .fill(fields.architecture); } if (fields.license) { await modal - .getByRole('textbox', { name: 'License (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'License' }) + .getByRole('textbox') .fill(fields.license); } if (fields.readme) { await modal - .getByRole('textbox', { name: 'README.md (optional)' }) + .locator('.ant-form-item') + .filter({ hasText: 'README.md' }) + .getByRole('textbox') .fill(fields.readme); } - // Access Level is required — select specified value or default to 'Internal' - const accessLevel = fields.accessLevel ?? 'Internal'; - await modal.getByRole('combobox', { name: 'Access Level' }).click(); - await expect( - this.page - .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') - .first(), - ).toBeVisible(); + // Access Level is required — select specified value or default to 'Private' (INTERNAL) + const accessLevel = fields.accessLevel ?? 'Private'; + // In antd v6, use the .ant-select-content to open the dropdown reliably. + await modal + .locator('.ant-form-item') + .filter({ hasText: 'Access Level' }) + .locator('.ant-select-content') + .click(); + // Ant Design Select renders the dropdown items as a portal outside the modal. + // Use the visible dropdown portal (not the ARIA-virtual options inside the combobox) + // to click the correct option. await this.page .locator('.ant-select-dropdown:not(.ant-select-dropdown-hidden)') - .first() - .locator('.ant-select-item-option') - .filter({ hasText: accessLevel }) + .getByText(accessLevel, { exact: true }) .click(); } @@ -289,6 +389,10 @@ export class AdminModelCardPage { // ── Delete Confirm Dialog helpers ──────────────────────────────────────── + getDeleteConfirmInput(): Locator { + return this.getDeleteConfirmDialog().getByRole('textbox'); + } + getDeleteConfirmButton(): Locator { return this.getDeleteConfirmDialog().getByRole('button', { name: 'Delete', @@ -301,11 +405,20 @@ export class AdminModelCardPage { }); } + getAlsoDeleteFolderCheckbox(): Locator { + return this.getDeleteConfirmDialog().getByRole('checkbox').first(); + } + + getFolderNameLinkInDeleteDialog(): Locator { + return this.getDeleteConfirmDialog().getByRole('link').first(); + } + // ── Helper: create via UI and return ───────────────────────────────────── async createModelCard(fields: { name: string; vfolderTitle?: string; + createNewFolderName?: string; }): Promise { await this.getCreateModelCardButton().click(); await expect(this.getCreateModal()).toBeVisible(); @@ -319,9 +432,10 @@ export class AdminModelCardPage { async deleteModelCardByName(name: string): Promise { await this.clickDeleteForRow(name); + await this.getDeleteConfirmInput().fill(name); await this.getDeleteConfirmButton().click(); await expect( - this.page.getByText('Model card has been deleted.'), - ).toBeVisible(); + this.page.getByText(/Model card has been deleted/), + ).toBeVisible({ timeout: 30000 }); } } diff --git a/e2e/utils/test-util.ts b/e2e/utils/test-util.ts index 19368edf16..a091d8cb48 100644 --- a/e2e/utils/test-util.ts +++ b/e2e/utils/test-util.ts @@ -401,9 +401,10 @@ export async function verifyVFolder( page: Page, folderName: string, statusTab: 'Active' | 'Trash' = 'Active', + dataPath: string = 'data', ) { // Use navigateTo for reliable navigation regardless of current page state - await navigateTo(page, 'data'); + await navigateTo(page, dataPath); await page.getByRole('tab', { name: statusTab }).click(); await clearAllFilters(page); await selectPropertyFilter(page, 'Name', folderName); @@ -472,9 +473,13 @@ export async function createVFolderAndVerify( await verifyVFolder(page, folderName); } -export async function moveToTrashAndVerify(page: Page, folderName: string) { +export async function moveToTrashAndVerify( + page: Page, + folderName: string, + dataPath: string = 'data', +) { // Use navigateTo to ensure a clean navigation to the data page regardless of current state - await navigateTo(page, 'data'); + await navigateTo(page, dataPath); await page.getByRole('tab', { name: 'Active' }).click(); await selectPropertyFilter(page, 'Name', folderName); @@ -490,15 +495,16 @@ export async function moveToTrashAndVerify(page: Page, folderName: string) { await expect(confirmButton).toBeVisible(); await confirmButton.click(); await removeSearchButton(page, folderName); - await verifyVFolder(page, folderName, 'Trash'); + await verifyVFolder(page, folderName, 'Trash', dataPath); } export async function deleteForeverAndVerifyFromTrash( page: Page, folderName: string, + dataPath: string = 'data', ) { // Use navigateTo to ensure a clean navigation to the data page regardless of current state - await navigateTo(page, 'data'); + await navigateTo(page, dataPath); await page.getByRole('tab', { name: 'Trash' }).click(); // Clear any existing filters before searching for the folder to delete