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