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