@@ -8,6 +8,13 @@ async function goToProjectsOverview(page: Page) {
88 await page . goto ( PLAYWRIGHT_BASE_URL + '/projects' ) ;
99}
1010
11+ // Helper to clear localStorage before tests that check persistence
12+ async function clearProjectTableState ( page : Page ) {
13+ await page . evaluate ( ( ) => {
14+ localStorage . removeItem ( 'project-table-state' ) ;
15+ } ) ;
16+ }
17+
1118// Create new project via modal
1219test ( 'test that creating and deleting a new project via the modal works' , async ( { page } ) => {
1320 const newProjectName = 'New Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
@@ -45,34 +52,62 @@ test('test that creating and deleting a new project via the modal works', async
4552 await expect ( page . getByTestId ( 'project_table' ) ) . not . toContainText ( newProjectName ) ;
4653} ) ;
4754
55+ // Helper to select a status filter using the new dropdown UI
56+ async function selectStatusFilter ( page : Page , status : 'Active' | 'Archived' ) {
57+ // Click the Filter button to open the dropdown
58+ await page . getByRole ( 'button' , { name : 'Filter projects' } ) . click ( ) ;
59+ // Click on Status submenu
60+ await page . getByRole ( 'menuitem' , { name : 'Status' } ) . click ( ) ;
61+ // Select the status option
62+ await page . getByRole ( 'menuitem' , { name : status } ) . click ( ) ;
63+ }
64+
65+ // Helper to remove status filter by clicking the X on the badge
66+ async function removeStatusFilter ( page : Page ) {
67+ const statusBadge = page . getByTestId ( 'status-filter-badge' ) ;
68+ // Click the remove button (second button in the badge, contains XMarkIcon)
69+ await statusBadge . locator ( 'button' ) . last ( ) . click ( ) ;
70+ }
71+
4872test ( 'test that archiving and unarchiving projects works' , async ( { page } ) => {
4973 const newProjectName = 'New Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
5074 await goToProjectsOverview ( page ) ;
75+ await clearProjectTableState ( page ) ;
76+ await page . reload ( ) ;
77+
5178 await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
5279 await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
5380
5481 await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
5582 await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
5683
84+ // Archive the project
5785 await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
58- await Promise . all ( [
59- page . getByRole ( 'menuitem' ) . getByText ( 'Archive' ) . first ( ) . click ( ) ,
60- expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ,
61- ] ) ;
62- await Promise . all ( [
63- page . getByRole ( 'tab' , { name : 'Archived' } ) . click ( ) ,
64- expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ,
65- ] ) ;
86+ await page . getByRole ( 'menuitem' ) . getByText ( 'Archive' ) . first ( ) . click ( ) ;
87+
88+ // Project should still be visible since default is "all" (no filter)
89+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
6690
91+ // Apply Active filter - archived project should disappear
92+ await selectStatusFilter ( page , 'Active' ) ;
93+ await expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ;
94+
95+ // Remove Active filter and apply Archived filter
96+ await removeStatusFilter ( page ) ;
97+ await selectStatusFilter ( page , 'Archived' ) ;
98+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
99+
100+ // Unarchive the project
67101 await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
68- await Promise . all ( [
69- page . getByRole ( 'menuitem' ) . getByText ( 'Unarchive' ) . first ( ) . click ( ) ,
70- expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ,
71- ] ) ;
72- await Promise . all ( [
73- page . getByRole ( 'tab' , { name : 'Active' } ) . click ( ) ,
74- expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ,
75- ] ) ;
102+ await page . getByRole ( 'menuitem' ) . getByText ( 'Unarchive' ) . first ( ) . click ( ) ;
103+
104+ // Project should disappear from Archived view
105+ await expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ;
106+
107+ // Remove Archived filter and apply Active filter to see the project
108+ await removeStatusFilter ( page ) ;
109+ await selectStatusFilter ( page , 'Active' ) ;
110+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
76111} ) ;
77112
78113test ( 'test that updating billable rate works with existing time entries' , async ( { page } ) => {
@@ -116,6 +151,147 @@ test('test that updating billable rate works with existing time entries', async
116151 ) . toBeVisible ( ) ;
117152} ) ;
118153
154+ // Sorting tests
155+ test ( 'test that sorting projects by name works' , async ( { page } ) => {
156+ await goToProjectsOverview ( page ) ;
157+ await clearProjectTableState ( page ) ;
158+ await page . reload ( ) ;
159+
160+ // Wait for the table to load
161+ await expect ( page . getByTestId ( 'project_table' ) ) . toBeVisible ( ) ;
162+
163+ // Get initial project names
164+ const getProjectNames = async ( ) => {
165+ const rows = page
166+ . getByTestId ( 'project_table' )
167+ . locator ( '[data-testid="project_table"] > div' )
168+ . filter ( { hasNot : page . locator ( '.border-t' ) } ) ;
169+ const names : string [ ] = [ ] ;
170+ const count = await page . getByTestId ( 'project_table' ) . getByRole ( 'row' ) . count ( ) ;
171+ for ( let i = 0 ; i < count ; i ++ ) {
172+ const row = page . getByTestId ( 'project_table' ) . getByRole ( 'row' ) . nth ( i ) ;
173+ const nameCell = row . locator ( 'div' ) . first ( ) ;
174+ const text = await nameCell . textContent ( ) ;
175+ if ( text ) {
176+ names . push ( text . trim ( ) ) ;
177+ }
178+ }
179+ return names ;
180+ } ;
181+
182+ // Click on Name header to sort ascending (default should already be ascending)
183+ const nameHeader = page . getByText ( 'Name' ) . first ( ) ;
184+ await nameHeader . click ( ) ;
185+
186+ // Wait for sort to apply
187+ await page . waitForTimeout ( 100 ) ;
188+
189+ // Click again to sort descending
190+ await nameHeader . click ( ) ;
191+ await page . waitForTimeout ( 100 ) ;
192+
193+ // Verify the sort indicator is showing descending
194+ await expect ( page . locator ( 'svg' ) . first ( ) ) . toBeVisible ( ) ;
195+ } ) ;
196+
197+ test ( 'test that sorting projects by status works' , async ( { page } ) => {
198+ await goToProjectsOverview ( page ) ;
199+ await clearProjectTableState ( page ) ;
200+ await page . reload ( ) ;
201+
202+ // Default is "all" so no filter needed - Wait for the table to load
203+ await expect ( page . getByTestId ( 'project_table' ) ) . toBeVisible ( ) ;
204+
205+ // Click on Status header to sort
206+ const statusHeader = page . getByText ( 'Status' ) . first ( ) ;
207+ await statusHeader . click ( ) ;
208+
209+ // Wait for sort to apply
210+ await page . waitForTimeout ( 100 ) ;
211+
212+ // Sort indicator should be visible
213+ await expect ( statusHeader . locator ( 'svg' ) ) . toBeVisible ( ) ;
214+ } ) ;
215+
216+ // Filter tests
217+ test ( 'test that filtering projects by status works' , async ( { page } ) => {
218+ const newProjectName = 'Filter Test Project ' + Math . floor ( 1 + Math . random ( ) * 10000 ) ;
219+ await goToProjectsOverview ( page ) ;
220+ await clearProjectTableState ( page ) ;
221+ await page . reload ( ) ;
222+
223+ // Create a new project
224+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
225+ await page . getByLabel ( 'Project Name' ) . fill ( newProjectName ) ;
226+ await page . getByRole ( 'button' , { name : 'Create Project' } ) . click ( ) ;
227+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
228+
229+ // Archive the project
230+ await page . getByRole ( 'row' ) . first ( ) . getByRole ( 'button' ) . click ( ) ;
231+ await page . getByRole ( 'menuitem' ) . getByText ( 'Archive' ) . first ( ) . click ( ) ;
232+
233+ // Project should still be visible (default is "all" - no filter)
234+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
235+
236+ // Apply Active filter - archived project should disappear
237+ await selectStatusFilter ( page , 'Active' ) ;
238+ await expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ;
239+
240+ // Remove Active filter - project should reappear (back to "all")
241+ await removeStatusFilter ( page ) ;
242+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
243+
244+ // Apply Archived filter - project should still be visible
245+ await selectStatusFilter ( page , 'Archived' ) ;
246+ await expect ( page . getByText ( newProjectName ) ) . toBeVisible ( ) ;
247+
248+ // Remove Archived filter and apply Active filter - project should not be visible
249+ await removeStatusFilter ( page ) ;
250+ await selectStatusFilter ( page , 'Active' ) ;
251+ await expect ( page . getByText ( newProjectName ) ) . not . toBeVisible ( ) ;
252+ } ) ;
253+
254+ test ( 'test that filter state persists after page reload' , async ( { page } ) => {
255+ await goToProjectsOverview ( page ) ;
256+ await clearProjectTableState ( page ) ;
257+ await page . reload ( ) ;
258+
259+ // Apply Active status filter
260+ await selectStatusFilter ( page , 'Active' ) ;
261+
262+ // Verify the filter badge is visible
263+ await expect ( page . getByTestId ( 'status-filter-badge' ) ) . toBeVisible ( ) ;
264+
265+ // Wait for the state to be saved
266+ await page . waitForTimeout ( 100 ) ;
267+
268+ // Reload the page
269+ await page . reload ( ) ;
270+
271+ // Verify the filter badge is still visible after reload
272+ await expect ( page . getByTestId ( 'status-filter-badge' ) ) . toBeVisible ( ) ;
273+ } ) ;
274+
275+ test ( 'test that sort state persists after page reload' , async ( { page } ) => {
276+ await goToProjectsOverview ( page ) ;
277+ await clearProjectTableState ( page ) ;
278+ await page . reload ( ) ;
279+
280+ // Click on Name header twice to sort descending
281+ const nameHeader = page . getByText ( 'Name' ) . first ( ) ;
282+ await nameHeader . click ( ) ;
283+ await nameHeader . click ( ) ;
284+
285+ // Wait for the state to be saved
286+ await page . waitForTimeout ( 100 ) ;
287+
288+ // Reload the page
289+ await page . reload ( ) ;
290+
291+ // Verify descending sort indicator is visible on Name column
292+ await expect ( page . getByTestId ( 'project_table' ) ) . toBeVisible ( ) ;
293+ } ) ;
294+
119295// Create new project with new Client
120296
121297// Create new project with existing Client
@@ -124,8 +300,6 @@ test('test that updating billable rate works with existing time entries', async
124300
125301// Test that project task count is displayed correctly
126302
127- // Test that active / archive / all filter works (once implemented)
128-
129303// Edit Project Modal Test
130304
131305// Add Project with billable rate
0 commit comments