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