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