Merge branch 'main' into 21720-material-ui-upgrade
[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         };
291
292         cy.loginAs(activeUser);
293         cy.goToPath(`/projects/${activeUser.user.uuid}`);
294         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
295         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
296         // Create new project
297         const projName = `Test project (${Math.floor(999999 * Math.random())})`;
298         createProject(projName, "Home project");
299         // Confirm that the user was taken to the newly created thing
300         cy.get("[data-cy=form-dialog]").should("not.exist");
301         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
302         cy.waitForDom();
303         cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
304         cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
305         // Create a subproject
306         const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
307         createProject(subProjName, projName);
308         cy.get("[data-cy=form-dialog]").should("not.exist");
309         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
310         cy.waitForDom();
311         cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
312         cy.get("[data-cy=breadcrumb-last]").should("contain", subProjName); //here
313     });
314
315     it("attempts to use a preexisting name creating a project", function () {
316         const name = `Test project ${Math.floor(Math.random() * 999999)}`;
317         cy.createGroup(activeUser.token, {
318             name: name,
319             group_class: "project",
320         });
321         cy.loginAs(activeUser);
322         cy.goToPath(`/projects/${activeUser.user.uuid}`);
323
324         // Attempt to create new collection with a duplicate name
325         cy.get("[data-cy=side-panel-button]").click();
326         cy.get("[data-cy=side-panel-new-project]").click();
327         cy.get("[data-cy=form-dialog]")
328             .should("contain", "New Project")
329             .within(() => {
330                 cy.get("[data-cy=name-field]").within(() => {
331                     cy.get("input").type(name);
332                 });
333                 cy.get("[data-cy=form-submit-btn]").click();
334             });
335         // Error message should display, allowing editing the name
336         cy.get("[data-cy=form-dialog]")
337             .should("exist") //here
338             .and("contain", "Project with the same name already exists")
339             .within(() => {
340                 cy.get("[data-cy=name-field]").within(() => {
341                     cy.get("input").type(" renamed");
342                 });
343                 cy.get("[data-cy=form-submit-btn]").click();
344                 cy.waitForDom();
345             });
346         cy.get("[data-cy=form-dialog]").should("not.exist");
347     });
348
349     it("navigates to the parent project after trashing the one being displayed", function () {
350         cy.createGroup(activeUser.token, {
351             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
352             group_class: "project",
353         })
354             .as("testRootProject")
355             .then(function () {
356                 cy.createGroup(activeUser.token, {
357                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
358                     group_class: "project",
359                     owner_uuid: this.testRootProject.uuid,
360                 }).as("testSubProject");
361             });
362         cy.getAll("@testRootProject", "@testSubProject").then(function ([testRootProject, testSubProject]) {
363             cy.loginAs(activeUser);
364
365             // Go to subproject and trash it.
366             cy.goToPath(`/projects/${testSubProject.uuid}`);
367             cy.get("[data-cy=side-panel-tree]").should("contain", testSubProject.name);
368             cy.waitForDom();
369             cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
370             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubProject.name).rightclick();
371             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
372
373             // Confirm that the parent project should be displayed.
374             cy.waitForDom();
375             cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
376             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
377             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
378             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
379
380             // Checks for bugfix #17637.
381             cy.get("[data-cy=not-found-content]").should("not.exist");
382             cy.get("[data-cy=not-found-page]").should("not.exist");
383         });
384     });
385
386     it("resets the search box only when navigating out of the current project", function () {
387         const fooProjectNameA = `Test foo project ${Math.floor(Math.random() * 999999)}`;
388         const fooProjectNameB = `Test foo project ${Math.floor(Math.random() * 999999)}`;
389         const barProjectNameA = `Test bar project ${Math.floor(Math.random() * 999999)}`;
390
391         [fooProjectNameA, fooProjectNameB, barProjectNameA].forEach(projName => {
392             cy.createGroup(activeUser.token, {
393                 name: projName,
394                 group_class: "project",
395             });
396         });
397
398         cy.loginAs(activeUser);
399         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("contain", barProjectNameA);
400
401         cy.get("[data-cy=search-input]").type("foo");
402         cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("not.contain", barProjectNameA);
403
404         // Click on the table row to select it, search should remain the same.
405             cy.get(`p:contains(${fooProjectNameA})`).should('exist').parents('td').click()
406             cy.get("[data-cy=search-input] input").should("have.value", "foo");
407
408         // Click to navigate to the project, search should be reset
409         cy.get(`p:contains(${fooProjectNameA})`).click();
410         cy.get("[data-cy=search-input] input").should("not.have.value", "foo");
411     });
412
413     it("navigates to the root project after trashing the parent of the one being displayed", function () {
414         cy.createGroup(activeUser.token, {
415             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
416             group_class: "project",
417         })
418             .as("testRootProject")
419             .then(function () {
420                 cy.createGroup(activeUser.token, {
421                     name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
422                     group_class: "project",
423                     owner_uuid: this.testRootProject.uuid,
424                 })
425                     .as("testSubProject")
426                     .then(function () {
427                         cy.createGroup(activeUser.token, {
428                             name: `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
429                             group_class: "project",
430                             owner_uuid: this.testSubProject.uuid,
431                         }).as("testSubSubProject");
432                     });
433             });
434         cy.getAll("@testRootProject", "@testSubProject", "@testSubSubProject").then(function ([testRootProject, testSubProject, testSubSubProject]) {
435             cy.loginAs(activeUser);
436
437             // Go to innermost project and trash its parent.
438             cy.goToPath(`/projects/${testSubSubProject.uuid}`);
439             cy.get("[data-cy=side-panel-tree]").should("contain", testSubSubProject.name);
440             cy.waitForDom();
441             cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
442             cy.get("[data-cy=breadcrumb-last]").should("contain", testSubSubProject.name);
443             cy.get("[data-cy=side-panel-tree]").contains(testSubProject.name).rightclick();
444             cy.get("[data-cy=context-menu]").contains("Move to trash").click();
445
446             // Confirm that the trashed project's parent should be displayed.
447             cy.waitForDom();
448             cy.get("[data-cy=breadcrumb-last]").should('exist', { timeout: 10000 });
449             cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
450             cy.url().should("contain", `/projects/${testRootProject.uuid}`);
451             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
452             cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubSubProject.name);
453
454             // Checks for bugfix #17637.
455             cy.get("[data-cy=not-found-content]").should("not.exist");
456             cy.get("[data-cy=not-found-page]").should("not.exist");
457         });
458     });
459
460     it("clears search input when changing project", () => {
461         cy.createGroup(activeUser.token, {
462             name: `Test root project ${Math.floor(Math.random() * 999999)}`,
463             group_class: "project",
464         })
465             .as("testProject1")
466             .then(testProject1 => {
467                 cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, "can_write");
468             });
469
470         cy.getAll("@testProject1").then(function ([testProject1]) {
471             cy.loginAs(activeUser);
472
473             cy.get("[data-cy=side-panel-tree]").contains(testProject1.name).click();
474
475             cy.get("[data-cy=search-input] input").type("test123");
476
477             cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
478
479             cy.get("[data-cy=search-input] input").should("not.have.value", "test123");
480         });
481     });
482
483     it("opens advanced popup for project with username", () => {
484         const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
485
486         cy.createGroup(adminUser.token, {
487             name: projectName,
488             group_class: "project",
489         }).as("mainProject");
490
491         cy.getAll("@mainProject").then(function ([mainProject]) {
492             cy.loginAs(adminUser);
493
494             cy.get("[data-cy=side-panel-tree]").contains("Groups").click();
495
496             cy.get("[data-cy=uuid]")
497                 .eq(0)
498                 .invoke("text")
499                 .then(uuid => {
500                     cy.createLink(adminUser.token, {
501                         name: "can_write",
502                         link_class: "permission",
503                         head_uuid: mainProject.uuid,
504                         tail_uuid: uuid,
505                     });
506
507                     cy.createLink(adminUser.token, {
508                         name: "can_write",
509                         link_class: "permission",
510                         head_uuid: mainProject.uuid,
511                         tail_uuid: activeUser.user.uuid,
512                     });
513
514                     cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
515
516                     cy.get("main").contains(projectName).rightclick();
517
518                     cy.get("[data-cy=context-menu]").contains("API Details").click();
519
520                     cy.get("[role=tablist]").contains("METADATA").click();
521
522                     cy.get("td").contains(uuid).should("exist");
523
524                     cy.get("td").contains(activeUser.user.uuid).should("exist");
525                 });
526         });
527     });
528
529     describe("Frozen projects", () => {
530         beforeEach(() => {
531             cy.createGroup(activeUser.token, {
532                 name: `Main project ${Math.floor(Math.random() * 999999)}`,
533                 group_class: "project",
534             }).as("mainProject");
535
536             cy.createGroup(adminUser.token, {
537                 name: `Admin project ${Math.floor(Math.random() * 999999)}`,
538                 group_class: "project",
539             })
540                 .as("adminProject")
541                 .then(mainProject => {
542                     cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, "can_write");
543                 });
544
545             cy.get("@mainProject").then(mainProject => {
546                 cy.createGroup(adminUser.token, {
547                     name: `Sub project ${Math.floor(Math.random() * 999999)}`,
548                     group_class: "project",
549                     owner_uuid: mainProject.uuid,
550                 }).as("subProject");
551
552                 cy.createCollection(adminUser.token, {
553                     name: `Main collection ${Math.floor(Math.random() * 999999)}`,
554                     owner_uuid: mainProject.uuid,
555                     manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
556                 }).as("mainCollection");
557             });
558         });
559
560         it("should be able to freeze own project", () => {
561             cy.getAll("@mainProject").then(([mainProject]) => {
562                 cy.loginAs(activeUser);
563
564                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
565
566                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
567
568                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
569
570                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
571             });
572         });
573
574         it("should not be able to modify items within the frozen project", () => {
575             cy.getAll("@mainProject", "@mainCollection").then(([mainProject, mainCollection]) => {
576                 cy.loginAs(activeUser);
577
578                 cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
579
580                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
581
582                 cy.get("[data-cy=project-panel]").contains(mainProject.name).click();
583
584                 cy.get("[data-cy=project-panel]").contains(mainCollection.name).rightclick();
585
586                 cy.get("[data-cy=context-menu]").contains("Move to trash").should("not.exist");
587             });
588         });
589
590         it("should be able to freeze not owned project", () => {
591             cy.getAll("@adminProject").then(([adminProject]) => {
592                 cy.loginAs(activeUser);
593
594                 cy.get("[data-cy=side-panel-tree]").contains("Shared with me").click();
595
596                 cy.get("main").contains(adminProject.name).rightclick();
597
598                 cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
599             });
600         });
601
602         it("should be able to unfreeze project if user is an admin", () => {
603             cy.getAll("@adminProject").then(([adminProject]) => {
604                 cy.loginAs(adminUser);
605
606                 cy.get("main").contains(adminProject.name).rightclick();
607
608                 cy.get("[data-cy=context-menu]").contains("Freeze").click();
609
610                 cy.wait(1000);
611
612                 cy.get("main").contains(adminProject.name).rightclick();
613
614                 cy.get("[data-cy=context-menu]").contains("Unfreeze").click();
615
616                 cy.get("main").contains(adminProject.name).rightclick();
617
618                 cy.get("[data-cy=context-menu]").contains("Freeze").should("exist");
619             });
620         });
621     });
622
623     // The following test is enabled on Electron only, as Chromium and Firefox
624     // require permissions to access the clipboard.
625     it("copies project URL to clipboard", { browser: 'electron' }, () => {
626         const projectName = `Test project (${Math.floor(999999 * Math.random())})`;
627
628         cy.loginAs(activeUser);
629         cy.get("[data-cy=side-panel-button]").click();
630         cy.get("[data-cy=side-panel-new-project]").click();
631         cy.get("[data-cy=form-dialog]")
632             .should("contain", "New Project")
633             .within(() => {
634                 cy.get("[data-cy=name-field]").within(() => {
635                     cy.get("input").type(projectName);
636                 });
637                 cy.get("[data-cy=form-submit-btn]").click();
638                 cy.waitForDom();
639             });
640         cy.get("[data-cy=form-dialog]").should("not.exist");
641         cy.get("[data-cy=snackbar]").contains("created");
642         cy.get("[data-cy=snackbar]").should("not.exist");
643         cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
644         cy.waitForDom();
645         cy.get("[data-cy=project-panel]").contains(projectName).should("be.visible").rightclick();
646         cy.get("[data-cy=context-menu]").contains("Copy link to clipboard").click();
647         cy.window({ timeout: 10000 }).then(win =>{
648             win.focus();
649             win.navigator.clipboard.readText().then(text => {
650                 expect(text).to.match(/https\:\/\/127\.0\.0\.1\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/);
651             })}
652         );
653     });
654
655     it("sorts displayed items correctly", () => {
656         cy.loginAs(activeUser);
657
658         cy.get('[data-cy=project-panel] button[aria-label="Select columns"]').click();
659         cy.get("div[role=presentation] ul > div[role=button]").contains("Date Created").click();
660         cy.get("div[role=presentation] ul > div[role=button]").contains("Trash at").click();
661         cy.get("div[role=presentation] ul > div[role=button]").contains("Delete at").click();
662         cy.get("div[role=presentation] > div[aria-hidden=true]").click();
663
664         cy.intercept({
665             method: "GET",
666             url: "**/arvados/v1/groups/*/contents*",
667             query: {
668                 // Ignore the count=exact itemsavailable request
669                 count: "none"
670             },
671         }).as("filteredQuery");
672         [
673             {
674                 name: "Name",
675                 asc: "collections.name asc,groups.name asc,workflows.name asc,created_at desc",
676                 desc: "collections.name desc,groups.name desc,workflows.name desc,created_at desc",
677             },
678             {
679                 name: "Last Modified",
680                 asc: "collections.modified_at asc,groups.modified_at asc,workflows.modified_at asc,created_at desc",
681                 desc: "collections.modified_at desc,groups.modified_at desc,workflows.modified_at desc,created_at desc",
682             },
683             {
684                 name: "Date Created",
685                 asc: "collections.created_at asc,groups.created_at asc,workflows.created_at asc,created_at desc",
686                 desc: "collections.created_at desc,groups.created_at desc,workflows.created_at desc,created_at desc",
687             },
688             {
689                 name: "Trash at",
690                 asc: "collections.trash_at asc,groups.trash_at asc,workflows.trash_at asc,created_at desc",
691                 desc: "collections.trash_at desc,groups.trash_at desc,workflows.trash_at desc,created_at desc",
692             },
693             {
694                 name: "Delete at",
695                 asc: "collections.delete_at asc,groups.delete_at asc,workflows.delete_at asc,created_at desc",
696                 desc: "collections.delete_at desc,groups.delete_at desc,workflows.delete_at desc,created_at desc",
697             },
698         ].forEach(test => {
699             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
700             cy.wait("@filteredQuery").then(interception => {
701                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
702                 expect(searchParams.get("order")).to.eq(test.asc);
703             });
704             cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
705             cy.wait("@filteredQuery").then(interception => {
706                 const searchParams = new URLSearchParams(new URL(interception.request.url).search);
707                 expect(searchParams.get("order")).to.eq(test.desc);
708             });
709         });
710     });
711 });