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