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