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