Merge branch 'main' into 21440-process-panel-reorg
[arvados.git] / services / workbench2 / 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('shows the appropriate buttons in the multiselect toolbar', () => {
184
185         const msButtonTooltips = [
186             'API Details',
187             'Add to Favorites',
188             'Copy to clipboard',
189             'Edit project',
190             'Freeze Project',
191             'Move to',
192             'Move to trash',
193             'New project',
194             'Open in new tab',
195             'Open with 3rd party client',
196             'Share',
197             'View details',
198         ];
199
200         cy.loginAs(activeUser);
201         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
202         cy.get('[data-cy=side-panel-button]').click();
203         cy.get('[data-cy=side-panel-new-project]').click();
204         cy.get('[data-cy=form-dialog]')
205             .should('contain', 'New Project')
206             .within(() => {
207                 cy.get('[data-cy=name-field]').within(() => {
208                     cy.get('input').type(projName);
209                 });
210             })
211         cy.get("[data-cy=form-submit-btn]").click();
212         cy.waitForDom()
213         cy.go('back')
214
215         cy.get('[data-cy=data-table-row]').contains(projName).should('exist').parent().parent().parent().click()
216         cy.waitForDom()
217         cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
218         for (let i = 0; i < msButtonTooltips.length; i++) {
219             cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
220             cy.get('body').contains(msButtonTooltips[i]).should('exist')
221             cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
222         }
223     })
224
225     it("creates new project on home project and then a subproject inside it", function () {
226         const createProject = function (name, parentName) {
227             cy.get("[data-cy=side-panel-button]").click();
228             cy.get("[data-cy=side-panel-new-project]").click();
229             cy.get("[data-cy=form-dialog]")
230                 .should("contain", "New Project")
231                 .within(() => {
232                     cy.get("[data-cy=parent-field]").within(() => {
233                         cy.get("input")
234                             .invoke("val")
235                             .then(val => {
236                                 expect(val).to.include(parentName);
237                             });
238                     });
239                     cy.get("[data-cy=name-field]").within(() => {
240                         cy.get("input").type(name);
241                     });
242                 });
243             cy.get("[data-cy=form-submit-btn]").click();
244         };
245
246         cy.loginAs(activeUser);
247         cy.goToPath(`/projects/${activeUser.user.uuid}`);
248         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
249         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
250         // Create new project
251         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
252         createProject(projName, "Home project");
253         // Confirm that the user was taken to the newly created thing
254         cy.get("[data-cy=form-dialog]").should("not.exist");
255         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
256         cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
257         // Create a subproject
258         const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
259         createProject(subProjName, projName);
260         cy.get("[data-cy=form-dialog]").should("not.exist");
261         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
262         cy.get("[data-cy=breadcrumb-last]").should("contain", subProjName);
263     });
264
265     it("attempts to use a preexisting name creating a project", function () {
266         const name = `Test project ${Math.floor(Math.random() * 999999)}`;
267         cy.createGroup(activeUser.token, {
268             name: name,
269             group_class: "project",
270         });
271         cy.loginAs(activeUser);
272         cy.goToPath(`/projects/${activeUser.user.uuid}`);
273
274         // Attempt to create new collection with a duplicate name
275         cy.get("[data-cy=side-panel-button]").click();
276         cy.get("[data-cy=side-panel-new-project]").click();
277         cy.get("[data-cy=form-dialog]")
278             .should("contain", "New Project")
279             .within(() => {
280                 cy.get("[data-cy=name-field]").within(() => {
281                     cy.get("input").type(name);
282                 });
283                 cy.get("[data-cy=form-submit-btn]").click();
284             });
285         // Error message should display, allowing editing the name
286         cy.get("[data-cy=form-dialog]")
287             .should("exist")
288             .and("contain", "Project with the same name already exists")
289             .within(() => {
290                 cy.get("[data-cy=name-field]").within(() => {
291                     cy.get("input").type(" renamed");
292                 });
293                 cy.get("[data-cy=form-submit-btn]").click();
294             });
295         cy.get("[data-cy=form-dialog]").should("not.exist");
296     });
297
298     it("navigates to the parent project after trashing the one being displayed", function () {
299         cy.createGroup(activeUser.token, {
300             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
301             group_class: "project",
302         })
303             .as("testRootProject")
304             .then(function () {
305                 cy.createGroup(activeUser.token, {
306                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
307                     group_class: "project",
308                     owner_uuid: this.testRootProject.uuid,
309                 }).as("testSubProject");
310             });
311         cy.getAll("@testRootProject", "@testSubProject").then(function ([testRootProject, testSubProject]) {
312             cy.loginAs(activeUser);
313
314             // Go to subproject and trash it.
315             cy.goToPath(`/projects/${testSubProject.uuid}`);
316             cy.get("[data-cy=side-panel-tree]").should("contain", testSubProject.name);
317             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubProject.name).rightclick();
318             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
319
320             // Confirm that the parent project should be displayed.
321             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
322             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
323             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
324
325             // Checks for bugfix #17637.
326             cy.get("[data-cy=not-found-content]").should("not.exist");
327             cy.get("[data-cy=not-found-page]").should("not.exist");
328         });
329     });
330
331     it("resets the search box only when navigating out of the current project", function () {
332         const fooProjectNameA = `Test foo project ${Math.floor(Math.random() * 999999)}`;
333         const fooProjectNameB = `Test foo project ${Math.floor(Math.random() * 999999)}`;
334         const barProjectNameA = `Test bar project ${Math.floor(Math.random() * 999999)}`;
335
336         [fooProjectNameA, fooProjectNameB, barProjectNameA].forEach(projName => {
337             cy.createGroup(activeUser.token, {
338                 name: projName,
339                 group_class: "project",
340             });
341         });
342
343         cy.loginAs(activeUser);
344         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("contain", barProjectNameA);
345
346         cy.get("[data-cy=search-input]").type("foo");
347         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("not.contain", barProjectNameA);
348
349         // Click on the table row to select it, search should remain the same.
350         cy.get(`p:contains(${fooProjectNameA})`).parent().parent().parent().parent().click();
351         cy.get("[data-cy=search-input] input").should("have.value", "foo");
352
353         // Click to navigate to the project, search should be reset
354         cy.get(`p:contains(${fooProjectNameA})`).click();
355         cy.get("[data-cy=search-input] input").should("not.have.value", "foo");
356     });
357
358     it("navigates to the root project after trashing the parent of the one being displayed", function () {
359         cy.createGroup(activeUser.token, {
360             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
361             group_class: "project",
362         })
363             .as("testRootProject")
364             .then(function () {
365                 cy.createGroup(activeUser.token, {
366                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
367                     group_class: "project",
368                     owner_uuid: this.testRootProject.uuid,
369                 })
370                     .as("testSubProject")
371                     .then(function () {
372                         cy.createGroup(activeUser.token, {
373                             name: `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
374                             group_class: "project",
375                             owner_uuid: this.testSubProject.uuid,
376                         }).as("testSubSubProject");
377                     });
378             });
379         cy.getAll("@testRootProject", "@testSubProject", "@testSubSubProject").then(function ([testRootProject, testSubProject, testSubSubProject]) {
380             cy.loginAs(activeUser);
381
382             // Go to innermost project and trash its parent.
383             cy.goToPath(`/projects/${testSubSubProject.uuid}`);
384             cy.get("[data-cy=side-panel-tree]").should("contain", testSubSubProject.name);
385             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubSubProject.name);
386             cy.get("[data-cy=side-panel-tree]").contains(testSubProject.name).rightclick();
387             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
388
389             // Confirm that the trashed project's parent should be displayed.
390             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
391             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
392             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
393             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubSubProject.name);
394
395             // Checks for bugfix #17637.
396             cy.get("[data-cy=not-found-content]").should("not.exist");
397             cy.get("[data-cy=not-found-page]").should("not.exist");
398         });
399     });
400
401     it("shows details panel when clicking on the info icon", () => {
402         cy.createGroup(activeUser.token, {
403             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
404             group_class: "project",
405         })
406             .as("testRootProject")
407             .then(function (testRootProject) {
408                 cy.loginAs(activeUser);
409
410                 cy.get("[data-cy=side-panel-tree]").contains(testRootProject.name).click();
411
412                 cy.get("[data-cy=additional-info-icon]").click();
413
414                 cy.contains(testRootProject.uuid).should("exist");
415             });
416     });
417
418     it("clears search input when changing project", () => {
419         cy.createGroup(activeUser.token, {
420             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
421             group_class: "project",
422         })
423             .as("testProject1")
424             .then(testProject1 => {
425                 cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, "can_write");
426             });
427
428         cy.getAll("@testProject1").then(function ([testProject1]) {
429             cy.loginAs(activeUser);
430
431             cy.get("[data-cy=side-panel-tree]").contains(testProject1.name).click();
432
433             cy.get("[data-cy=search-input] input").type("test123");
434
435             cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
436
437             cy.get("[data-cy=search-input] input").should("not.have.value", "test123");
438         });
439     });
440
441     it("opens advanced popup for project with username", () => {
442         const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
443
444         cy.createGroup(adminUser.token, {
445             name: projectName,
446             group_class: "project",
447         }).as("mainProject");
448
449         cy.getAll("@mainProject").then(function ([mainProject]) {
450             cy.loginAs(adminUser);
451
452             cy.get("[data-cy=side-panel-tree]").contains("Groups").click();
453
454             cy.get("[data-cy=uuid]")
455                 .eq(0)
456                 .invoke("text")
457                 .then(uuid => {
458                     cy.createLink(adminUser.token, {
459                         name: "can_write",
460                         link_class: "permission",
461                         head_uuid: mainProject.uuid,
462                         tail_uuid: uuid,
463                     });
464
465                     cy.createLink(adminUser.token, {
466                         name: "can_write",
467                         link_class: "permission",
468                         head_uuid: mainProject.uuid,
469                         tail_uuid: activeUser.user.uuid,
470                     });
471
472                     cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
473
474                     cy.get("main").contains(projectName).rightclick();
475
476                     cy.get("[data-cy=context-menu]").contains("API Details").click();
477
478                     cy.get("[role=tablist]").contains("METADATA").click();
479
480                     cy.get("td").contains(uuid).should("exist");
481
482                     cy.get("td").contains(activeUser.user.uuid).should("exist");
483                 });
484         });
485     });
486
487     describe("Frozen projects", () => {
488         beforeEach(() => {
489             cy.createGroup(activeUser.token, {
490                 name: `Main project ${Math.floor(Math.random() * 999999)}`,
491                 group_class: "project",
492             }).as("mainProject");
493
494             cy.createGroup(adminUser.token, {
495                 name: `Admin project ${Math.floor(Math.random() * 999999)}`,
496                 group_class: "project",
497             })
498                 .as("adminProject")
499                 .then(mainProject => {
500                     cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, "can_write");
501                 });
502
503             cy.get("@mainProject").then(mainProject => {
504                 cy.createGroup(adminUser.token, {
505                     name: `Sub project ${Math.floor(Math.random() * 999999)}`,
506                     group_class: "project",
507                     owner_uuid: mainProject.uuid,
508                 }).as("subProject");
509
510                 cy.createCollection(adminUser.token, {
511                     name: `Main collection ${Math.floor(Math.random() * 999999)}`,
512                     owner_uuid: mainProject.uuid,
513                     manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
514                 }).as("mainCollection");
515             });
516         });
517
518         it("should be able to freeze own project", () => {
519             cy.getAll("@mainProject").then(([mainProject]) => {
520                 cy.loginAs(activeUser);
521
522                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
523
524                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
525
526                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
527
528                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
529             });
530         });
531
532         it("should not be able to modify items within the frozen project", () => {
533             cy.getAll("@mainProject", "@mainCollection").then(([mainProject, mainCollection]) => {
534                 cy.loginAs(activeUser);
535
536                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
537
538                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
539
540                 cy.get("[data-cy=project-panel]").contains(mainProject.name).click();
541
542                 cy.get("[data-cy=project-panel]").contains(mainCollection.name).rightclick();
543
544                 cy.get("[data-cy=context-menu]").contains("Move to trash").should("not.exist");
545             });
546         });
547
548         it("should be able to freeze not owned project", () => {
549             cy.getAll("@adminProject").then(([adminProject]) => {
550                 cy.loginAs(activeUser);
551
552                 cy.get("[data-cy=side-panel-tree]").contains("Shared with me").click();
553
554                 cy.get("main").contains(adminProject.name).rightclick();
555
556                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
557             });
558         });
559
560         it("should be able to unfreeze project if user is an admin", () => {
561             cy.getAll("@adminProject").then(([adminProject]) => {
562                 cy.loginAs(adminUser);
563
564                 cy.get("main").contains(adminProject.name).rightclick();
565
566                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
567
568                 cy.wait(1000);
569
570                 cy.get("main").contains(adminProject.name).rightclick();
571
572                 cy.get("[data-cy=context-menu]").contains("Unfreeze").click();
573
574                 cy.get("main").contains(adminProject.name).rightclick();
575
576                 cy.get("[data-cy=context-menu]").contains("Freeze").should("exist");
577             });
578         });
579     });
580
581     it("copies project URL to clipboard", () => {
582         const projectName = `Test project (${Math.floor(999999 * Math.random())})`;
583
584         cy.loginAs(activeUser);
585         cy.get("[data-cy=side-panel-button]").click();
586         cy.get("[data-cy=side-panel-new-project]").click();
587         cy.get("[data-cy=form-dialog]")
588             .should("contain", "New Project")
589             .within(() => {
590                 cy.get("[data-cy=name-field]").within(() => {
591                     cy.get("input").type(projectName);
592                 });
593                 cy.get("[data-cy=form-submit-btn]").click();
594             });
595         cy.get("[data-cy=form-dialog]").should("not.exist");
596         cy.get("[data-cy=snackbar]").contains("created");
597         cy.get("[data-cy=snackbar]").should("not.exist");
598         cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
599         cy.waitForDom();
600         cy.get("[data-cy=project-panel]").contains(projectName).should("be.visible").rightclick();
601         cy.get("[data-cy=context-menu]").contains("Copy to clipboard").click();
602         cy.window().then(win =>
603             win.navigator.clipboard.readText().then(text => {
604                 expect(text).to.match(/https\:\/\/127\.0\.0\.1\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/);
605             })
606         );
607     });
608
609     it("sorts displayed items correctly", () => {
610         cy.loginAs(activeUser);
611
612         cy.get('[data-cy=project-panel] button[title="Select columns"]').click();
613         cy.get("div[role=presentation] ul > div[role=button]").contains("Date Created").click();
614         cy.get("div[role=presentation] ul > div[role=button]").contains("Trash at").click();
615         cy.get("div[role=presentation] ul > div[role=button]").contains("Delete at").click();
616         cy.get("div[role=presentation] > div[aria-hidden=true]").click();
617
618         cy.intercept({ method: "GET", url: "**/arvados/v1/groups/*/contents*" }).as("filteredQuery");
619         [
620             {
621                 name: "Name",
622                 asc: "collections.name asc,container_requests.name asc,groups.name asc,container_requests.created_at desc",
623                 desc: "collections.name desc,container_requests.name desc,groups.name desc,container_requests.created_at desc",
624             },
625             {
626                 name: "Last Modified",
627                 asc: "collections.modified_at asc,container_requests.modified_at asc,groups.modified_at asc,container_requests.created_at desc",
628                 desc: "collections.modified_at desc,container_requests.modified_at desc,groups.modified_at desc,container_requests.created_at desc",
629             },
630             {
631                 name: "Date Created",
632                 asc: "collections.created_at asc,container_requests.created_at asc,groups.created_at asc,container_requests.created_at desc",
633                 desc: "collections.created_at desc,container_requests.created_at desc,groups.created_at desc,container_requests.created_at desc",
634             },
635             {
636                 name: "Trash at",
637                 asc: "collections.trash_at asc,container_requests.trash_at asc,groups.trash_at asc,container_requests.created_at desc",
638                 desc: "collections.trash_at desc,container_requests.trash_at desc,groups.trash_at desc,container_requests.created_at desc",
639             },
640             {
641                 name: "Delete at",
642                 asc: "collections.delete_at asc,container_requests.delete_at asc,groups.delete_at asc,container_requests.created_at desc",
643                 desc: "collections.delete_at desc,container_requests.delete_at desc,groups.delete_at desc,container_requests.created_at desc",
644             },
645         ].forEach(test => {
646             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
647             cy.wait("@filteredQuery").then(interception => {
648                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
649                 expect(searchParams.get("order")).to.eq(test.asc);
650             });
651             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
652             cy.wait("@filteredQuery").then(interception => {
653                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
654                 expect(searchParams.get("order")).to.eq(test.desc);
655             });
656         });
657     });
658 });