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