Merge branch 'main' into 19302-left-side-panel-changes
[arvados-workbench2.git] / cypress / integration / project.spec.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 describe("Project tests", function () {
6     let activeUser;
7     let adminUser;
8
9     before(function () {
10         // Only set up common users once. These aren't set up as aliases because
11         // aliases are cleaned up after every test. Also it doesn't make sense
12         // to set the same users on beforeEach() over and over again, so we
13         // separate a little from Cypress' 'Best Practices' here.
14         cy.getUser("admin", "Admin", "User", true, true)
15             .as("adminUser")
16             .then(function () {
17                 adminUser = this.adminUser;
18             });
19         cy.getUser("user", "Active", "User", false, true)
20             .as("activeUser")
21             .then(function () {
22                 activeUser = this.activeUser;
23             });
24     });
25
26     beforeEach(function () {
27         cy.clearCookies();
28         cy.clearLocalStorage();
29     });
30
31     it("creates a new project with multiple properties", function () {
32         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
33         cy.loginAs(activeUser);
34         cy.get("[data-cy=side-panel-button]").click();
35         cy.get("[data-cy=side-panel-new-project]").click();
36         cy.get("[data-cy=form-dialog]")
37             .should("contain", "New Project")
38             .within(() => {
39                 cy.get("[data-cy=name-field]").within(() => {
40                     cy.get("input").type(projName);
41                 });
42             });
43         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
44         cy.get("[data-cy=form-dialog]").should("not.contain", "Color: Magenta");
45         cy.get("[data-cy=resource-properties-form]").within(() => {
46             cy.get("[data-cy=property-field-key]").within(() => {
47                 cy.get("input").type("Color");
48             });
49             cy.get("[data-cy=property-field-value]").within(() => {
50                 cy.get("input").type("Magenta");
51             });
52             cy.root().submit();
53             cy.get("[data-cy=property-field-value]").within(() => {
54                 cy.get("input").type("Pink");
55             });
56             cy.root().submit();
57             cy.get("[data-cy=property-field-value]").within(() => {
58                 cy.get("input").type("Yellow");
59             });
60             cy.root().submit();
61         });
62         // Confirm proper vocabulary labels are displayed on the UI.
63         cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
64         cy.get("[data-cy=form-dialog]").should("contain", "Color: Pink");
65         cy.get("[data-cy=form-dialog]").should("contain", "Color: Yellow");
66
67         cy.get("[data-cy=resource-properties-form]").within(() => {
68             cy.get("[data-cy=property-field-key]").within(() => {
69                 cy.get("input").focus();
70             });
71             cy.get("[data-cy=property-field-key]").should("not.contain", "Color");
72         });
73
74         // Create project and confirm the properties' real values.
75         cy.get("[data-cy=form-submit-btn]").click();
76         cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
77         cy.doRequest("GET", "/arvados/v1/groups", null, {
78             filters: `[["name", "=", "${projName}"], ["group_class", "=", "project"]]`,
79         })
80             .its("body.items")
81             .as("projects")
82             .then(function () {
83                 expect(this.projects).to.have.lengthOf(1);
84                 expect(this.projects[0].properties).to.deep.equal(
85                     // Pink is not in the test vocab
86                     { IDTAGCOLORS: ["IDVALCOLORS3", "Pink", "IDVALCOLORS1"] }
87                 );
88             });
89
90         // Open project edit via breadcrumbs
91         cy.get("[data-cy=breadcrumbs]").contains(projName).rightclick();
92         cy.get("[data-cy=context-menu]").contains("Edit").click();
93         cy.get("[data-cy=form-dialog]").within(() => {
94             cy.get("[data-cy=resource-properties-list]").within(() => {
95                 cy.get("div[role=button]").contains("Color: Magenta");
96                 cy.get("div[role=button]").contains("Color: Pink");
97                 cy.get("div[role=button]").contains("Color: Yellow");
98             });
99         });
100         // Add another property
101         cy.get("[data-cy=resource-properties-form]").within(() => {
102             cy.get("[data-cy=property-field-key]").within(() => {
103                 cy.get("input").type("Animal");
104             });
105             cy.get("[data-cy=property-field-value]").within(() => {
106                 cy.get("input").type("Dog");
107             });
108             cy.root().submit();
109         });
110         cy.get("[data-cy=form-submit-btn]").click({ force: true });
111         // Reopen edit via breadcrumbs and verify properties
112         cy.get("[data-cy=breadcrumbs]").contains(projName).rightclick();
113         cy.get("[data-cy=context-menu]").contains("Edit").click();
114         cy.get("[data-cy=form-dialog]").within(() => {
115             cy.get("[data-cy=resource-properties-list]").within(() => {
116                 cy.get("div[role=button]").contains("Color: Magenta");
117                 cy.get("div[role=button]").contains("Color: Pink");
118                 cy.get("div[role=button]").contains("Color: Yellow");
119                 cy.get("div[role=button]").contains("Animal: Dog");
120             });
121         });
122     });
123
124     it("creates a project without and with description", function () {
125         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
126         cy.loginAs(activeUser);
127
128         // Create project
129         cy.get("[data-cy=side-panel-button]").click();
130         cy.get("[data-cy=side-panel-new-project]").click();
131         cy.get("[data-cy=form-dialog]")
132             .should("contain", "New Project")
133             .within(() => {
134                 cy.get("[data-cy=name-field]").within(() => {
135                     cy.get("input").type(projName);
136                 });
137             });
138         cy.get("[data-cy=form-submit-btn]").click();
139         cy.get("[data-cy=form-dialog]").should("not.exist");
140
141         const editProjectDescription = (name, type) => {
142             cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
143             cy.get("[data-cy=project-panel] tbody tr").contains(name).rightclick({ force: true });
144             cy.get("[data-cy=context-menu]").contains("Edit").click();
145             cy.get("[data-cy=form-dialog]").within(() => {
146                 cy.get("div[contenteditable=true]").click().type(type);
147                 cy.get("[data-cy=form-submit-btn]").click();
148             });
149         };
150
151         const verifyProjectDescription = (name, description) => {
152             cy.doRequest("GET", "/arvados/v1/groups", null, {
153                 filters: `[["name", "=", "${name}"], ["group_class", "=", "project"]]`,
154             })
155                 .its("body.items")
156                 .as("projects")
157                 .then(function () {
158                     expect(this.projects).to.have.lengthOf(1);
159                     expect(this.projects[0].description).to.equal(description);
160                 });
161         };
162
163         // Edit description
164         editProjectDescription(projName, "Test description");
165
166         // Check description is set
167         verifyProjectDescription(projName, "<p>Test description</p>");
168
169         // Clear description
170         editProjectDescription(projName, "{selectall}{backspace}");
171
172         // Check description is null
173         verifyProjectDescription(projName, null);
174
175         // Set description to contain whitespace
176         editProjectDescription(projName, "{selectall}{backspace}    x");
177         editProjectDescription(projName, "{backspace}");
178
179         // Check description is null
180         verifyProjectDescription(projName, null);
181     });
182
183     it("creates new project on home project and then a subproject inside it", function () {
184         const createProject = function (name, parentName) {
185             cy.get("[data-cy=side-panel-button]").click();
186             cy.get("[data-cy=side-panel-new-project]").click();
187             cy.get("[data-cy=form-dialog]")
188                 .should("contain", "New Project")
189                 .within(() => {
190                     cy.get("[data-cy=parent-field]").within(() => {
191                         cy.get("input")
192                             .invoke("val")
193                             .then(val => {
194                                 expect(val).to.include(parentName);
195                             });
196                     });
197                     cy.get("[data-cy=name-field]").within(() => {
198                         cy.get("input").type(name);
199                     });
200                 });
201             cy.get("[data-cy=form-submit-btn]").click();
202         };
203
204         cy.loginAs(activeUser);
205         cy.goToPath(`/projects/${activeUser.user.uuid}`);
206         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
207         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
208         // Create new project
209         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
210         createProject(projName, "Home project");
211         // Confirm that the user was taken to the newly created thing
212         cy.get("[data-cy=form-dialog]").should("not.exist");
213         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
214         cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
215         // Create a subproject
216         const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
217         createProject(subProjName, projName);
218         cy.get("[data-cy=form-dialog]").should("not.exist");
219         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
220         cy.get("[data-cy=breadcrumb-last]").should("contain", subProjName);
221     });
222
223     it("attempts to use a preexisting name creating a project", function () {
224         const name = `Test project ${Math.floor(Math.random() * 999999)}`;
225         cy.createGroup(activeUser.token, {
226             name: name,
227             group_class: "project",
228         });
229         cy.loginAs(activeUser);
230         cy.goToPath(`/projects/${activeUser.user.uuid}`);
231
232         // Attempt to create new collection with a duplicate name
233         cy.get("[data-cy=side-panel-button]").click();
234         cy.get("[data-cy=side-panel-new-project]").click();
235         cy.get("[data-cy=form-dialog]")
236             .should("contain", "New Project")
237             .within(() => {
238                 cy.get("[data-cy=name-field]").within(() => {
239                     cy.get("input").type(name);
240                 });
241                 cy.get("[data-cy=form-submit-btn]").click();
242             });
243         // Error message should display, allowing editing the name
244         cy.get("[data-cy=form-dialog]")
245             .should("exist")
246             .and("contain", "Project with the same name already exists")
247             .within(() => {
248                 cy.get("[data-cy=name-field]").within(() => {
249                     cy.get("input").type(" renamed");
250                 });
251                 cy.get("[data-cy=form-submit-btn]").click();
252             });
253         cy.get("[data-cy=form-dialog]").should("not.exist");
254     });
255
256     it("navigates to the parent project after trashing the one being displayed", function () {
257         cy.createGroup(activeUser.token, {
258             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
259             group_class: "project",
260         })
261             .as("testRootProject")
262             .then(function () {
263                 cy.createGroup(activeUser.token, {
264                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
265                     group_class: "project",
266                     owner_uuid: this.testRootProject.uuid,
267                 }).as("testSubProject");
268             });
269         cy.getAll("@testRootProject", "@testSubProject").then(function ([testRootProject, testSubProject]) {
270             cy.loginAs(activeUser);
271
272             // Go to subproject and trash it.
273             cy.goToPath(`/projects/${testSubProject.uuid}`);
274             cy.get("[data-cy=side-panel-tree]").should("contain", testSubProject.name);
275             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubProject.name).rightclick();
276             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
277
278             // Confirm that the parent project should be displayed.
279             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
280             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
281             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
282
283             // Checks for bugfix #17637.
284             cy.get("[data-cy=not-found-content]").should("not.exist");
285             cy.get("[data-cy=not-found-page]").should("not.exist");
286         });
287     });
288
289     it("resets the search box only when navigating out of the current project", function () {
290         const fooProjectNameA = `Test foo project ${Math.floor(Math.random() * 999999)}`;
291         const fooProjectNameB = `Test foo project ${Math.floor(Math.random() * 999999)}`;
292         const barProjectNameA = `Test bar project ${Math.floor(Math.random() * 999999)}`;
293
294         [fooProjectNameA, fooProjectNameB, barProjectNameA].forEach(projName => {
295             cy.createGroup(activeUser.token, {
296                 name: projName,
297                 group_class: "project",
298             });
299         });
300
301         cy.loginAs(activeUser);
302         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("contain", barProjectNameA);
303
304         cy.get("[data-cy=search-input]").type("foo");
305         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("not.contain", barProjectNameA);
306
307         // Click on the table row to select it, search should remain the same.
308         cy.get(`p:contains(${fooProjectNameA})`).parent().parent().parent().parent().click();
309         cy.get("[data-cy=search-input] input").should("have.value", "foo");
310
311         // Click to navigate to the project, search should be reset
312         cy.get(`p:contains(${fooProjectNameA})`).click();
313         cy.get("[data-cy=search-input] input").should("not.have.value", "foo");
314     });
315
316     it("navigates to the root project after trashing the parent of the one being displayed", function () {
317         cy.createGroup(activeUser.token, {
318             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
319             group_class: "project",
320         })
321             .as("testRootProject")
322             .then(function () {
323                 cy.createGroup(activeUser.token, {
324                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
325                     group_class: "project",
326                     owner_uuid: this.testRootProject.uuid,
327                 })
328                     .as("testSubProject")
329                     .then(function () {
330                         cy.createGroup(activeUser.token, {
331                             name: `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
332                             group_class: "project",
333                             owner_uuid: this.testSubProject.uuid,
334                         }).as("testSubSubProject");
335                     });
336             });
337         cy.getAll("@testRootProject", "@testSubProject", "@testSubSubProject").then(function ([testRootProject, testSubProject, testSubSubProject]) {
338             cy.loginAs(activeUser);
339
340             // Go to innermost project and trash its parent.
341             cy.goToPath(`/projects/${testSubSubProject.uuid}`);
342             cy.get("[data-cy=side-panel-tree]").should("contain", testSubSubProject.name);
343             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubSubProject.name);
344             cy.get("[data-cy=side-panel-tree]").contains(testSubProject.name).rightclick();
345             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
346
347             // Confirm that the trashed project's parent should be displayed.
348             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
349             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
350             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
351             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubSubProject.name);
352
353             // Checks for bugfix #17637.
354             cy.get("[data-cy=not-found-content]").should("not.exist");
355             cy.get("[data-cy=not-found-page]").should("not.exist");
356         });
357     });
358
359     it("shows details panel when clicking on the info icon", () => {
360         cy.createGroup(activeUser.token, {
361             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
362             group_class: "project",
363         })
364             .as("testRootProject")
365             .then(function (testRootProject) {
366                 cy.loginAs(activeUser);
367
368                 cy.get("[data-cy=side-panel-tree]").contains(testRootProject.name).click();
369
370                 cy.get("[data-cy=additional-info-icon]").click();
371
372                 cy.contains(testRootProject.uuid).should("exist");
373             });
374     });
375
376     it("clears search input when changing project", () => {
377         cy.createGroup(activeUser.token, {
378             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
379             group_class: "project",
380         })
381             .as("testProject1")
382             .then(testProject1 => {
383                 cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, "can_write");
384             });
385
386         cy.getAll("@testProject1").then(function ([testProject1]) {
387             cy.loginAs(activeUser);
388
389             cy.get("[data-cy=side-panel-tree]").contains(testProject1.name).click();
390
391             cy.get("[data-cy=search-input] input").type("test123");
392
393             cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
394
395             cy.get("[data-cy=search-input] input").should("not.have.value", "test123");
396         });
397     });
398
399     it("opens advanced popup for project with username", () => {
400         const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
401
402         cy.createGroup(adminUser.token, {
403             name: projectName,
404             group_class: "project",
405         }).as("mainProject");
406
407         cy.getAll("@mainProject").then(function ([mainProject]) {
408             cy.loginAs(adminUser);
409
410             cy.get("[data-cy=side-panel-tree]").contains("Groups").click();
411
412             cy.get("[data-cy=uuid]")
413                 .eq(0)
414                 .invoke("text")
415                 .then(uuid => {
416                     cy.createLink(adminUser.token, {
417                         name: "can_write",
418                         link_class: "permission",
419                         head_uuid: mainProject.uuid,
420                         tail_uuid: uuid,
421                     });
422
423                     cy.createLink(adminUser.token, {
424                         name: "can_write",
425                         link_class: "permission",
426                         head_uuid: mainProject.uuid,
427                         tail_uuid: activeUser.user.uuid,
428                     });
429
430                     cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
431
432                     cy.get("main").contains(projectName).rightclick();
433
434                     cy.get("[data-cy=context-menu]").contains("API Details").click();
435
436                     cy.get("[role=tablist]").contains("METADATA").click();
437
438                     cy.get("td").contains(uuid).should("exist");
439
440                     cy.get("td").contains(activeUser.user.uuid).should("exist");
441                 });
442         });
443     });
444
445     describe("Frozen projects", () => {
446         beforeEach(() => {
447             cy.createGroup(activeUser.token, {
448                 name: `Main project ${Math.floor(Math.random() * 999999)}`,
449                 group_class: "project",
450             }).as("mainProject");
451
452             cy.createGroup(adminUser.token, {
453                 name: `Admin project ${Math.floor(Math.random() * 999999)}`,
454                 group_class: "project",
455             })
456                 .as("adminProject")
457                 .then(mainProject => {
458                     cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, "can_write");
459                 });
460
461             cy.get("@mainProject").then(mainProject => {
462                 cy.createGroup(adminUser.token, {
463                     name: `Sub project ${Math.floor(Math.random() * 999999)}`,
464                     group_class: "project",
465                     owner_uuid: mainProject.uuid,
466                 }).as("subProject");
467
468                 cy.createCollection(adminUser.token, {
469                     name: `Main collection ${Math.floor(Math.random() * 999999)}`,
470                     owner_uuid: mainProject.uuid,
471                     manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
472                 }).as("mainCollection");
473             });
474         });
475
476         it("should be able to froze own project", () => {
477             cy.getAll("@mainProject").then(([mainProject]) => {
478                 cy.loginAs(activeUser);
479
480                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
481
482                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
483
484                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
485
486                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
487             });
488         });
489
490         it("should not be able to modify items within the frozen project", () => {
491             cy.getAll("@mainProject", "@mainCollection").then(([mainProject, mainCollection]) => {
492                 cy.loginAs(activeUser);
493
494                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
495
496                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
497
498                 cy.get("[data-cy=project-panel]").contains(mainProject.name).click();
499
500                 cy.get("[data-cy=project-panel]").contains(mainCollection.name).rightclick();
501
502                 cy.get("[data-cy=context-menu]").contains("Move to trash").should("not.exist");
503             });
504         });
505
506         it("should be able to froze not owned project", () => {
507             cy.getAll("@adminProject").then(([adminProject]) => {
508                 cy.loginAs(activeUser);
509
510                 cy.get("[data-cy=side-panel-tree]").contains("Shared with me").click();
511
512                 cy.get("main").contains(adminProject.name).rightclick();
513
514                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
515             });
516         });
517
518         it("should be able to unfroze project if user is an admin", () => {
519             cy.getAll("@adminProject").then(([adminProject]) => {
520                 cy.loginAs(adminUser);
521
522                 cy.get("main").contains(adminProject.name).rightclick();
523
524                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
525
526                 cy.wait(1000);
527
528                 cy.get("main").contains(adminProject.name).rightclick();
529
530                 cy.get("[data-cy=context-menu]").contains("Unfreeze").click();
531
532                 cy.get("main").contains(adminProject.name).rightclick();
533
534                 cy.get("[data-cy=context-menu]").contains("Freeze").should("exist");
535             });
536         });
537     });
538
539     it("copies project URL to clipboard", () => {
540         const projectName = `Test project (${Math.floor(999999 * Math.random())})`;
541
542         cy.loginAs(activeUser);
543         cy.get("[data-cy=side-panel-button]").click();
544         cy.get("[data-cy=side-panel-new-project]").click();
545         cy.get("[data-cy=form-dialog]")
546             .should("contain", "New Project")
547             .within(() => {
548                 cy.get("[data-cy=name-field]").within(() => {
549                     cy.get("input").type(projectName);
550                 });
551                 cy.get("[data-cy=form-submit-btn]").click();
552             });
553         cy.get("[data-cy=form-dialog]").should("not.exist");
554         cy.get("[data-cy=snackbar]").contains("created");
555         cy.get("[data-cy=snackbar]").should("not.exist");
556         cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
557         cy.waitForDom();
558         cy.get("[data-cy=project-panel]").contains(projectName).should("be.visible").rightclick();
559         cy.get("[data-cy=context-menu]").contains("Copy to clipboard").click();
560         cy.window().then(win =>
561             win.navigator.clipboard.readText().then(text => {
562                 expect(text).to.match(/https\:\/\/127\.0\.0\.1\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/);
563             })
564         );
565     });
566
567     it.only("sorts displayed items correctly", () => {
568         cy.loginAs(activeUser);
569
570         cy.get('[data-cy=project-panel] button[title="Select columns"]').click();
571         cy.get("div[role=presentation] ul > div[role=button]").contains("Date Created").click();
572         cy.get("div[role=presentation] ul > div[role=button]").contains("Trash at").click();
573         cy.get("div[role=presentation] ul > div[role=button]").contains("Delete at").click();
574         cy.get("div[role=presentation] > div[aria-hidden=true]").click();
575
576         cy.intercept({ method: "GET", url: "**/arvados/v1/groups/*/contents*" }).as("filteredQuery");
577         [
578             {
579                 name: "Name",
580                 asc: "collections.name asc,container_requests.name asc,groups.name asc,container_requests.created_at desc",
581                 desc: "collections.name desc,container_requests.name desc,groups.name desc,container_requests.created_at desc",
582             },
583             {
584                 name: "Last Modified",
585                 asc: "collections.modified_at asc,container_requests.modified_at asc,groups.modified_at asc,container_requests.created_at desc",
586                 desc: "collections.modified_at desc,container_requests.modified_at desc,groups.modified_at desc,container_requests.created_at desc",
587             },
588             {
589                 name: "Date Created",
590                 asc: "collections.created_at asc,container_requests.created_at asc,groups.created_at asc,container_requests.created_at desc",
591                 desc: "collections.created_at desc,container_requests.created_at desc,groups.created_at desc,container_requests.created_at desc",
592             },
593             {
594                 name: "Trash at",
595                 asc: "collections.trash_at asc,container_requests.trash_at asc,groups.trash_at asc,container_requests.created_at desc",
596                 desc: "collections.trash_at desc,container_requests.trash_at desc,groups.trash_at desc,container_requests.created_at desc",
597             },
598             {
599                 name: "Delete at",
600                 asc: "collections.delete_at asc,container_requests.delete_at asc,groups.delete_at asc,container_requests.created_at desc",
601                 desc: "collections.delete_at desc,container_requests.delete_at desc,groups.delete_at desc,container_requests.created_at desc",
602             },
603         ].forEach(test => {
604             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
605             cy.wait("@filteredQuery").then(interception => {
606                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
607                 expect(searchParams.get("order")).to.eq(test.asc);
608             });
609             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
610             cy.wait("@filteredQuery").then(interception => {
611                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
612                 expect(searchParams.get("order")).to.eq(test.desc);
613             });
614         });
615     });
616 });