Skip to content

Commit 486e23b

Browse files
committed
feat: implement Cypress E2E test infrastructure and Phase 1 test coverage
- Bug fixes: fix error tab query param (errored → error), add CardBody testid - Test cleanup: add DELETE /api/v1/test/push/:id and /user/:username routes (NODE_ENV=test only) - Custom commands: cy.deleteTestPush(), cy.deleteTestUser() for test data cleanup - UI instrumentation: add data-testid attributes to 16+ components for robust selectors - New test files: repo-details, push-requests, repo-list, profile, user-list, settings, navigation, error-pages (42 tests total) - Backfill cleanup: add afterEach/after cleanup to push-details and pushActions tests
1 parent 6981427 commit 486e23b

31 files changed

Lines changed: 1647 additions & 33 deletions

CYPRESS_PLAN.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# Cypress E2E Test Plan
2+
3+
## Goal
4+
Cover all functionality and UI elements on each page to minimize UI-related bugs and regression.
5+
6+
## Existing Coverage
7+
8+
| Page | Test File | Coverage |
9+
|------|-----------|----------|
10+
| Login | `login.cy.js` | Logo, inputs, valid/invalid login, redirect |
11+
| Repo List | `repo.cy.js` | Add repo, duplicate error, anonymous/regular/admin permissions, clone tooltip |
12+
| Push Actions | `docker/pushActions.cy.js` | Approve, Reject, Cancel, unauthorized attempts, dialog cancel |
13+
| Auto-Approved Push | `autoApproved.cy.js` | Auto-approved message, tooltip timestamp |
14+
| Push Details | `push-details.cy.js` | Pending/Approved/Rejected/Canceled states, tabs, card body, steps, error state, navigation |
15+
16+
---
17+
18+
## Prerequisites: Bug Fixes & Infrastructure
19+
20+
### Bug Fixes Required
21+
22+
| Bug | Location | Fix |
23+
|-----|----------|-----|
24+
| Error tab sends wrong query param | `src/ui/views/PushRequests/components/PushesTable.tsx:64` | Change `errored``error` to match DB field |
25+
| Broken CardBody selector in test | `cypress/e2e/push-details.cy.js:84` | Add `data-testid="push-details-card-body"` to `PushDetails.tsx` `<CardBody>` |
26+
27+
### Test Data Cleanup Infrastructure
28+
29+
**Problem:** `cy.createPush()` creates permanent push records via real git operations, but there is no API to delete a push. Tests accumulate data forever.
30+
31+
**Solution:**
32+
1. Add `src/service/routes/test.ts` with test-only endpoints (gated by `NODE_ENV === 'test'`):
33+
- `DELETE /api/v1/test/push/:id` — calls `db.deletePush(id)` (admin auth)
34+
- `DELETE /api/v1/test/user/:username` — calls `db.deleteUser(username)` (admin auth)
35+
2. Conditionally mount in `src/service/routes/index.ts` when `NODE_ENV === 'test'`
36+
3. Add custom commands to `cypress/support/commands.js`:
37+
- `cy.deleteTestPush(pushId)`
38+
- `cy.deleteTestUser(username)`
39+
4. Backfill cleanup into existing leaky test files:
40+
- `push-details.cy.js` — add `afterEach` push cleanup
41+
- `docker/pushActions.cy.js` — add `afterEach` push cleanup + `after` user cleanup
42+
43+
### UI Instrumentation Needed
44+
45+
Systematically add `data-testid` and accessibility attributes to untested pages for robust, maintainable selectors:
46+
47+
| File | Attributes to Add |
48+
|------|-------------------|
49+
| `src/ui/views/PushDetails/PushDetails.tsx` | `data-testid="push-details-card-body"` on `<CardBody>` |
50+
| `src/ui/views/PushRequests/PushRequests.tsx` | `data-testid="push-requests-tabs"` on tabs container |
51+
| `src/ui/views/PushRequests/components/PushesTable.tsx` | `data-testid="push-row-<id>"` on each `<TableRow>`, `data-testid="pushes-table"` on table |
52+
| `src/ui/views/RepoDetails/RepoDetails.tsx` | `data-testid="repo-info-card"`, `data-testid="reviewers-table"`, `data-testid="contributors-table"`, `data-testid="add-reviewer-btn"`, `data-testid="add-contributor-btn"`, `data-testid="code-clone-btn"` |
53+
| `src/ui/views/RepoDetails/Components/AddUser.tsx` | `data-testid="add-user-dialog"`, `data-testid="add-user-select"`, `data-testid="add-user-confirm-btn"` |
54+
| `src/ui/views/RepoDetails/Components/DeleteRepoDialog.tsx` | `data-testid="delete-repo-dialog"`, `data-testid="delete-repo-confirm-input"`, `data-testid="delete-repo-confirm-btn"` |
55+
| `src/ui/views/User/UserProfile.tsx` | `data-testid="profile-name"`, `data-testid="profile-role"`, `data-testid="profile-email"`, `data-testid="profile-gitAccount"`, `data-testid="profile-admin-status"`, `data-testid="gitAccount-input"`, `data-testid="update-profile-btn"` |
56+
| `src/ui/views/UserList/Components/UserList.tsx` | `data-testid="user-list-table"`, `data-testid="user-row-<username>"` on each row |
57+
| `src/ui/views/Settings/Settings.tsx` | `data-testid="jwt-token-input"`, `data-testid="jwt-token-toggle"`, `data-testid="jwt-save-btn"`, `data-testid="jwt-clear-btn"`, `data-testid="settings-snackbar"` |
58+
| `src/ui/components/Sidebar/Sidebar.tsx` | `aria-current="page"` on active `<NavLink>` |
59+
| `src/ui/components/Navbars/Navbar.tsx` | `data-testid="navbar"` |
60+
| `src/ui/components/Footer/Footer.tsx` | `data-testid="footer"` |
61+
| `src/ui/components/Search/Search.tsx` | `data-testid="search-input"` |
62+
| `src/ui/components/Pagination/Pagination.tsx` | `data-testid="pagination-previous"`, `data-testid="pagination-next"`, `data-testid="pagination-info"` |
63+
| `src/ui/components/Filtering/Filtering.tsx` | `data-testid="filter-dropdown"`, `data-testid="filter-option-<name>"`, `data-testid="filter-sort-toggle"` |
64+
| `src/ui/views/Extras/NotFound.tsx` | `data-testid="not-found-page"` |
65+
| `src/ui/views/Extras/NotAuthorized.tsx` | `data-testid="not-authorized-page"` |
66+
67+
---
68+
69+
## Phase 1: High-Complexity Pages
70+
71+
### 1. Push Details — Tabs & Content Rendering ✅ DONE
72+
**Route:** `/dashboard/push/:id`
73+
**File:** `cypress/e2e/push-details.cy.js`
74+
**Strategy:** Real API for 10/11 tests, intercept only for error state. Cleanup added via `afterEach`.
75+
76+
- [x] 1.1 — Pending push shows Pending status with action buttons *(real API)*
77+
- [x] 1.2 — Card body renders: Timestamp, Remote Head link, Commit SHA link, Repository link, Branch link *(real API)*
78+
- [x] 1.3 — Commits tab renders commit data table with correct columns *(real API)*
79+
- [x] 1.4 — Changes tab renders diff content via diff2html *(real API)*
80+
- [x] 1.5 — Steps tab renders steps timeline with summary chips *(real API)*
81+
- [x] 1.6 — Steps accordions expand and show content/logs *(real API)*
82+
- [x] 1.7 — Rejected push shows rejection info with reason *(real API)*
83+
- [x] 1.8 — Approved push shows attestation info *(real API)*
84+
- [x] 1.9 — Error state renders error message when API fails *(intercept — can't trigger real 500)*
85+
- [x] 1.10 — Canceled push shows Canceled status *(real API)*
86+
- [x] 1.11 — Action buttons navigate back to push list after completing action *(real API)*
87+
88+
### 2. Repo Details — User Management
89+
**Route:** `/dashboard/repo/:id`
90+
**File:** `cypress/e2e/repo-details.cy.js`
91+
**Strategy:** Real API for all tests. Create a test repo via `cy.request POST /api/v1/repo` in `before`, clean up in `after`.
92+
93+
- [ ] 2.1 — Repo info renders: project, name, URL links
94+
- [ ] 2.2 — Reviewers table renders user list with links
95+
- [ ] 2.3 — Contributors table renders user list with links
96+
- [ ] 2.4 — Admin can add reviewer via "Add Reviewer" button
97+
- [ ] 2.5 — Admin can remove reviewer
98+
- [ ] 2.6 — Admin can add contributor via "Add Contributor" button
99+
- [ ] 2.7 — Admin can remove contributor
100+
- [ ] 2.8 — Delete repo dialog opens, confirms, navigates to repo list
101+
- [ ] 2.9 — Non-admin cannot see add/remove/delete buttons
102+
- [ ] 2.10 — Code clone button renders with correct URL
103+
104+
### 3. Push Requests — Tab Filtering
105+
**Route:** `/dashboard/push`
106+
**File:** `cypress/e2e/push-requests.cy.js`
107+
**Strategy:** Shared dataset created once in `before()`, cleaned up in `after()`. Uses real pushes for Pending/Approved/Rejected/Canceled, intercept for Error tab. *Comment in code explaining shared dataset for PR reviewers.*
108+
109+
- [ ] 3.1 — All 6 tabs render (All, Pending, Approved, Canceled, Rejected, Error)
110+
- [ ] 3.2 — Pending tab filters to show only pending pushes *(real API)*
111+
- [ ] 3.3 — Approved tab filters to show only approved pushes *(real API)*
112+
- [ ] 3.4 — Canceled tab filters to show only canceled pushes *(real API)*
113+
- [ ] 3.5 — Rejected tab filters to show only rejected pushes *(real API)*
114+
- [ ] 3.6 — Error tab filters to show only errored pushes *(intercept — requires UI bugfix + synthetic error push)*
115+
- [ ] 3.7 — Push table rows are clickable and navigate to Push Details *(real API)*
116+
117+
### 4. Repo List — Search, Filter, Pagination
118+
**Route:** `/dashboard/repo`
119+
**File:** `cypress/e2e/repo-list.cy.js`
120+
**Strategy:** Create 6+ test repos via fast API (`cy.request POST /api/v1/repo`) in `before()`, clean up in `after()`. Pagination is tested here only (shared `Pagination` component). Search/filter use client-side logic.
121+
122+
- [ ] 4.1 — Search filters repos by name
123+
- [ ] 4.2 — Search filters repos by project
124+
- [ ] 4.3 — Clear search resets to all repos
125+
- [ ] 4.4 — Filter dropdown sorts by Date Modified, Date Created, Alphabetical
126+
- [ ] 4.5 — Pagination renders and navigates between pages
127+
- [ ] 4.6 — Repo rows are clickable and navigate to Repo Details
128+
129+
### 5. Profile Page
130+
**Route:** `/dashboard/profile`
131+
**File:** `cypress/e2e/profile.cy.js`
132+
**Strategy:** Real API for all tests.
133+
134+
- [ ] 5.1 — Displays user info: name, role, email, GitHub username, admin status
135+
- [ ] 5.2 — User can edit their own GitHub username
136+
- [ ] 5.3 — Admin can edit another user's GitHub username (via `/dashboard/user/:id`)
137+
- [ ] 5.4 — Non-admin viewing another user's profile cannot edit
138+
139+
### 6. User List (Admin)
140+
**Route:** `/dashboard/admin/user`
141+
**File:** `cypress/e2e/user-list.cy.js`
142+
**Strategy:** Real API. *Note: Create/delete user UI does not exist; tests cover only read access.*
143+
144+
- [ ] 6.1 — Renders list of all users
145+
- ~~6.2 — Admin can create a new user~~ *(UI not implemented — removed from scope)*
146+
- ~~6.3 — Admin can delete a user~~ *(UI not implemented — removed from scope)*
147+
- [ ] 6.4 — Non-admin cannot access user list
148+
149+
### 7. Settings Page
150+
**Route:** `/dashboard/admin/settings`
151+
**File:** `cypress/e2e/settings.cy.js`
152+
**Strategy:** Uses `localStorage` for JWT persistence. No backend API calls for save/clear.
153+
154+
- [ ] 7.1 — JWT token field renders with show/hide toggle
155+
- [ ] 7.2 — Save button persists token and shows snackbar
156+
- [ ] 7.3 — Clear button removes token and shows snackbar
157+
- [ ] 7.4 — Token persists across page reload
158+
159+
### 8. Navigation & Shell
160+
**File:** `cypress/e2e/navigation.cy.js`
161+
**Strategy:** Mix of real navigation and intercepts.
162+
163+
- [ ] 8.1 — Sidebar renders all visible links (Repositories, Dashboard, My Account, Users, Settings)
164+
- [ ] 8.2 — Sidebar links navigate correctly
165+
- [ ] 8.3 — Active sidebar item highlights *(uses `aria-current="page"`)*
166+
- [ ] 8.4 — Navbar renders correctly
167+
- [ ] 8.5 — Footer renders
168+
- [ ] 8.6 — Unauthenticated user is redirected to `/login`
169+
- [ ] 8.7 — `/` redirects to `/dashboard/repo`
170+
171+
### 9. Error Pages
172+
**File:** `cypress/e2e/error-pages.cy.js`
173+
**Strategy:** Direct navigation. No API needed.
174+
175+
- [ ] 9.1 — Unknown route shows 404 page
176+
- [ ] 9.2 — Unauthorized route shows NotAuthorized page
177+
178+
---
179+
180+
## Implementation Notes
181+
182+
### Hybrid Approach: Real API First, Intercepts as Fallback
183+
184+
- **Prefer real API calls** — leverage existing custom commands (`cy.createPush()`, `cy.createUser()`, `cy.addUserPushPermission()`, etc.) to create real data and test real UI rendering
185+
- **Use `cy.intercept()` only when real API is impractical** — e.g., mocking 500 errors, testing edge-case data shapes (empty commits, specific step errors/blocks), or simulating OIDC flows
186+
- **Shared datasets for read-only filtering tests** — acceptable when tests only assert on rendering, not mutations. Document with inline comments.
187+
- Use `cy.session()` for login (already available in custom commands)
188+
- Follow existing file naming convention: `cypress/e2e/<name>.cy.js`
189+
- Include Apache 2.0 license header in all new files
190+
- Each test file should document which tests use real API vs intercepts (see `push-details.cy.js` as reference)
191+
192+
### Cleanup Discipline
193+
194+
- Every test that creates a push via `cy.createPush()` must clean it up via `cy.deleteTestPush()` in `afterEach` or `after`
195+
- Every test that creates a user via `cy.createUser()` should clean it up via `cy.deleteTestUser()` in `after`
196+
- `repo-list.cy.js` creates repos via `cy.request POST /api/v1/repo`; clean up via `cy.deleteRepo()` in `after`
197+
- Do not rely on database wipes between CI runs; keep local repeated runs safe

cypress/e2e/docker/pushActions.cy.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,20 @@ describe('Push Actions (Approve, Reject, Cancel)', () => {
4949
cy.logout();
5050
});
5151

52-
afterEach(() => {
52+
afterEach(function () {
53+
// Clean up push created in this test (if any)
54+
if (this.pushId) {
55+
cy.deleteTestPush(this.pushId);
56+
}
5357
cy.logout();
5458
});
5559

60+
after(() => {
61+
// Clean up test users
62+
cy.deleteTestUser(testUser.username);
63+
cy.deleteTestUser(approverUser.username);
64+
});
65+
5666
describe('Approve flow', () => {
5767
beforeEach(() => {
5868
const suffix = `approve-${Date.now()}`;

cypress/e2e/error-pages.cy.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright 2026 GitProxy Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Error Pages
19+
* Strategy: Direct navigation. No API needed.
20+
*/
21+
describe('Error Pages', () => {
22+
beforeEach(() => {
23+
cy.login('admin', 'admin');
24+
});
25+
26+
afterEach(() => {
27+
cy.logout();
28+
});
29+
30+
// --- 9.1 Unknown route shows 404 ---
31+
it('9.1 — Unknown route shows 404 page', () => {
32+
cy.visit('/dashboard/nonexistent-page-xyz');
33+
34+
cy.get('[data-testid="not-found-page"]').should('be.visible');
35+
cy.contains('404').should('be.visible');
36+
});
37+
38+
// --- 9.2 Unauthorized route shows NotAuthorized ---
39+
it('9.2 — Unauthorized route shows NotAuthorized page', () => {
40+
// Create a non-admin user and try to access admin route
41+
const regularUser = {
42+
username: `errorpage_user_${Date.now()}`,
43+
password: 'pass123',
44+
email: `errorpage_${Date.now()}@example.com`,
45+
gitAccount: `errorpage_git_${Date.now()}`,
46+
};
47+
48+
cy.request({
49+
method: 'POST',
50+
url: `${Cypress.env('API_BASE_URL') || Cypress.config('baseUrl')}/api/auth/create-user`,
51+
body: regularUser,
52+
failOnStatusCode: false,
53+
});
54+
55+
cy.logout();
56+
cy.login(regularUser.username, regularUser.password);
57+
cy.visit('/dashboard/admin/settings');
58+
59+
// Should show not authorized page
60+
cy.get('[data-testid="not-authorized-page"]').should('be.visible');
61+
cy.contains('403').should('be.visible');
62+
63+
// Clean up user
64+
cy.logout();
65+
cy.deleteTestUser(regularUser.username);
66+
});
67+
});

cypress/e2e/navigation.cy.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Copyright 2026 GitProxy Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Navigation & Shell
19+
* Strategy: Mix of real navigation and intercepts.
20+
*/
21+
describe('Navigation & Shell', () => {
22+
beforeEach(() => {
23+
cy.login('admin', 'admin');
24+
});
25+
26+
afterEach(() => {
27+
cy.logout();
28+
});
29+
30+
// --- 8.1 Sidebar renders all visible links ---
31+
it('8.1 — Sidebar renders all visible links', () => {
32+
cy.visit('/dashboard/repo');
33+
34+
// Sidebar links should be present
35+
cy.contains('Repositories').should('be.visible');
36+
cy.contains('Dashboard').should('be.visible');
37+
});
38+
39+
// --- 8.2 Sidebar links navigate correctly ---
40+
it('8.2 — Sidebar links navigate correctly', () => {
41+
cy.visit('/dashboard/repo');
42+
43+
// Navigate to push dashboard
44+
cy.contains('Dashboard').click();
45+
cy.url().should('include', '/dashboard/push');
46+
47+
// Navigate back to repos
48+
cy.contains('Repositories').click();
49+
cy.url().should('include', '/dashboard/repo');
50+
});
51+
52+
// --- 8.3 Active sidebar item highlights ---
53+
it('8.3 — Active sidebar item highlights', () => {
54+
cy.visit('/dashboard/repo');
55+
56+
// The active nav link should have aria-current="page"
57+
cy.get('[aria-current="page"]').should('exist');
58+
});
59+
60+
// --- 8.4 Navbar renders ---
61+
it('8.4 — Navbar renders correctly', () => {
62+
cy.visit('/dashboard/repo');
63+
64+
cy.get('[data-testid="navbar"]').should('be.visible');
65+
});
66+
67+
// --- 8.5 Footer renders ---
68+
it('8.5 — Footer renders', () => {
69+
cy.visit('/dashboard/repo');
70+
71+
cy.get('[data-testid="footer"]').should('be.visible');
72+
});
73+
74+
// --- 8.6 Unauthenticated user redirected ---
75+
it('8.6 — Unauthenticated user is redirected to /login', () => {
76+
cy.logout();
77+
cy.visit('/dashboard/repo');
78+
79+
cy.url().should('include', '/login');
80+
});
81+
82+
// --- 8.7 Root redirects to dashboard/repo ---
83+
it('8.7 — / redirects to /dashboard/repo', () => {
84+
cy.logout();
85+
cy.visit('/');
86+
87+
// Root should redirect (either to login if not authenticated, or to dashboard/repo)
88+
cy.url().should('match', /\/(login|dashboard)/);
89+
});
90+
});

0 commit comments

Comments
 (0)