Merge branch '21461-excessive-scrollbars-fix'. Closes #21461
[arvados.git] / services / workbench2 / cypress / e2e / collection.cy.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 const path = require("path");
6
7 describe("Collection panel tests", function () {
8     let activeUser;
9     let adminUser;
10     let downloadsFolder;
11
12     before(function () {
13         // Only set up common users once. These aren't set up as aliases because
14         // aliases are cleaned up after every test. Also it doesn't make sense
15         // to set the same users on beforeEach() over and over again, so we
16         // separate a little from Cypress' 'Best Practices' here.
17         cy.getUser("admin", "Admin", "User", true, true)
18             .as("adminUser")
19             .then(function () {
20                 adminUser = this.adminUser;
21             });
22         cy.getUser("collectionuser1", "Collection", "User", false, true)
23             .as("activeUser")
24             .then(function () {
25                 activeUser = this.activeUser;
26             });
27         downloadsFolder = Cypress.config("downloadsFolder");
28     });
29
30     it('shows the appropriate buttons in the toolbar', () => {
31
32         const msButtonTooltips = [
33             'API Details',
34             'Add to Favorites',
35             'Copy to clipboard',
36             'Edit collection',
37             'Make a copy',
38             'Move to',
39             'Move to trash',
40             'Open in new tab',
41             'Open with 3rd party client',
42             'Share',
43             'View details',
44         ];
45
46         cy.loginAs(activeUser);
47         const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
48         cy.get("[data-cy=side-panel-button]").click({force: true});
49         cy.get("[data-cy=side-panel-new-collection]").click();
50         cy.get("[data-cy=form-dialog]")
51             .should("contain", "New collection")
52             .within(() => {
53                 cy.get("[data-cy=name-field]").within(() => {
54                     cy.get("input").type(name);
55                 });
56                 cy.get("[data-cy=form-submit-btn]").click();
57             });
58             cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
59             cy.waitForDom()
60             cy.get('[data-cy=data-table-row]').contains(name).should('exist').parent().parent().parent().parent().click()
61             cy.waitForDom()
62             cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
63             for (let i = 0; i < msButtonTooltips.length; i++) {
64                 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
65                 cy.get('body').contains(msButtonTooltips[i]).should('exist')
66                 cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
67             }
68     })
69
70     it("allows to download mountain duck config for a collection", () => {
71         cy.createCollection(adminUser.token, {
72             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
73             owner_uuid: activeUser.user.uuid,
74             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
75         })
76             .as("testCollection")
77             .then(function (testCollection) {
78                 cy.loginAs(activeUser);
79                 cy.goToPath(`/collections/${testCollection.uuid}`);
80
81                 cy.get("[data-cy=collection-panel-options-btn]").click();
82                 cy.get("[data-cy=context-menu]").contains("Open with 3rd party client").click();
83                 cy.get("[data-cy=download-button").click();
84
85                 const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
86
87                 cy.readFile(filename, { timeout: 15000 })
88                     .then(body => {
89                         const childrenCollection = Array.prototype.slice.call(Cypress.$(body).find("dict")[0].children);
90                         const map = {};
91                         let i,
92                             j = 2;
93
94                         for (i = 0; i < childrenCollection.length; i += j) {
95                             map[childrenCollection[i].outerText] = childrenCollection[i + 1].outerText;
96                         }
97
98                         cy.get("#simple-tabpanel-0")
99                             .find("a")
100                             .then(a => {
101                                 const [host, port] = a.text().split("@")[1].split("/")[0].split(":");
102                                 expect(map["Protocol"]).to.equal("davs");
103                                 expect(map["UUID"]).to.equal(testCollection.uuid);
104                                 expect(map["Username"]).to.equal(activeUser.user.username);
105                                 expect(map["Port"]).to.equal(port);
106                                 expect(map["Hostname"]).to.equal(host);
107                                 if (map["Path"]) {
108                                     expect(map["Path"]).to.equal(`/c=${testCollection.uuid}`);
109                                 }
110                             });
111                     })
112                     .then(() => cy.task("clearDownload", { filename }));
113             });
114     });
115
116     it("attempts to use a preexisting name creating or updating a collection", function () {
117         const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
118         cy.createCollection(adminUser.token, {
119             name: name,
120             owner_uuid: activeUser.user.uuid,
121             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
122         });
123         cy.loginAs(activeUser);
124         cy.goToPath(`/projects/${activeUser.user.uuid}`);
125         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
126         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
127         // Attempt to create new collection with a duplicate name
128         cy.get("[data-cy=side-panel-button]").click();
129         cy.get("[data-cy=side-panel-new-collection]").click();
130         cy.get("[data-cy=form-dialog]")
131             .should("contain", "New collection")
132             .within(() => {
133                 cy.get("[data-cy=name-field]").within(() => {
134                     cy.get("input").type(name);
135                 });
136                 cy.get("[data-cy=form-submit-btn]").click();
137             });
138         // Error message should display, allowing editing the name
139         cy.get("[data-cy=form-dialog]")
140             .should("exist")
141             .and("contain", "Collection with the same name already exists")
142             .within(() => {
143                 cy.get("[data-cy=name-field]").within(() => {
144                     cy.get("input").type(" renamed");
145                 });
146                 cy.get("[data-cy=form-submit-btn]").click();
147             });
148         cy.get("[data-cy=form-dialog]").should("not.exist");
149         // Attempt to rename the collection with the duplicate name
150         cy.get("[data-cy=collection-panel-options-btn]").click();
151         cy.get("[data-cy=context-menu]").contains("Edit collection").click();
152         cy.get("[data-cy=form-dialog]")
153             .should("contain", "Edit Collection")
154             .within(() => {
155                 cy.get("[data-cy=name-field]").within(() => {
156                     cy.get("input").type("{selectall}{backspace}").type(name);
157                 });
158                 cy.get("[data-cy=form-submit-btn]").click();
159             });
160         cy.get("[data-cy=form-dialog]").should("exist").and("contain", "Collection with the same name already exists");
161     });
162
163     it("uses the property editor (from edit dialog) with vocabulary terms", function () {
164         cy.createCollection(adminUser.token, {
165             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
166             owner_uuid: activeUser.user.uuid,
167             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
168         })
169             .as("testCollection")
170             .then(function () {
171                 cy.loginAs(activeUser);
172                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
173
174                 cy.get("[data-cy=collection-info-panel").should("contain", this.testCollection.name).and("not.contain", "Color: Magenta");
175
176                 cy.get("[data-cy=collection-panel-options-btn]").click();
177                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
178                 cy.get("[data-cy=form-dialog]").should("contain", "Properties");
179
180                 // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
181                 cy.get("[data-cy=resource-properties-form]").within(() => {
182                     cy.get("[data-cy=property-field-key]").within(() => {
183                         cy.get("input").type("Color");
184                     });
185                     cy.get("[data-cy=property-field-value]").within(() => {
186                         cy.get("input").type("Magenta");
187                     });
188                     cy.root().submit();
189                 });
190                 // Confirm proper vocabulary labels are displayed on the UI.
191                 cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
192                 cy.get("[data-cy=form-dialog]").contains("Save").click();
193                 cy.get("[data-cy=form-dialog]").should("not.exist");
194                 // Confirm proper vocabulary IDs were saved on the backend.
195                 cy.doRequest("GET", `/arvados/v1/collections/${this.testCollection.uuid}`)
196                     .its("body")
197                     .as("collection")
198                     .then(function () {
199                         expect(this.collection.properties.IDTAGCOLORS).to.equal("IDVALCOLORS3");
200                     });
201                 // Confirm the property is displayed on the UI.
202                 cy.get("[data-cy=collection-info-panel").should("contain", this.testCollection.name).and("contain", "Color: Magenta");
203             });
204     });
205
206     it("uses the editor (from details panel) with vocabulary terms", function () {
207         cy.createCollection(adminUser.token, {
208             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
209             owner_uuid: activeUser.user.uuid,
210             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
211         })
212             .as("testCollection")
213             .then(function () {
214                 cy.loginAs(activeUser);
215                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
216
217                 cy.get("[data-cy=collection-info-panel")
218                     .should("contain", this.testCollection.name)
219                     .and("not.contain", "Color: Magenta")
220                     .and("not.contain", "Size: S");
221                 cy.get("[data-cy=additional-info-icon]").click();
222
223                 cy.get("[data-cy=details-panel]").within(() => {
224                     cy.get("[data-cy=details-panel-edit-btn]").click();
225                 });
226                 cy.get("[data-cy=form-dialog").contains("Edit Collection");
227
228                 // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
229                 cy.get("[data-cy=resource-properties-form]").within(() => {
230                     cy.get("[data-cy=property-field-key]").within(() => {
231                         cy.get("input").type("Color");
232                     });
233                     cy.get("[data-cy=property-field-value]").within(() => {
234                         cy.get("input").type("Magenta");
235                     });
236                     cy.root().submit();
237                 });
238                 // Confirm proper vocabulary labels are displayed on the UI.
239                 cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
240
241                 // Case-insensitive on-blur auto-selection test
242                 // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
243                 cy.get("[data-cy=resource-properties-form]").within(() => {
244                     cy.get("[data-cy=property-field-key]").within(() => {
245                         cy.get("input").type("sIzE");
246                     });
247                     cy.get("[data-cy=property-field-value]").within(() => {
248                         cy.get("input").type("sMaLL");
249                     });
250                     // Cannot "type()" TAB on Cypress so let's click another field
251                     // to trigger the onBlur event.
252                     cy.get("[data-cy=property-field-key]").click();
253                     cy.root().submit();
254                 });
255                 // Confirm proper vocabulary labels are displayed on the UI.
256                 cy.get("[data-cy=form-dialog]").should("contain", "Size: S");
257
258                 cy.get("[data-cy=form-dialog]").contains("Save").click();
259                 cy.get("[data-cy=form-dialog]").should("not.exist");
260
261                 // Confirm proper vocabulary IDs were saved on the backend.
262                 cy.doRequest("GET", `/arvados/v1/collections/${this.testCollection.uuid}`)
263                     .its("body")
264                     .as("collection")
265                     .then(function () {
266                         expect(this.collection.properties.IDTAGCOLORS).to.equal("IDVALCOLORS3");
267                         expect(this.collection.properties.IDTAGSIZES).to.equal("IDVALSIZES2");
268                     });
269
270                 // Confirm properties display on the UI.
271                 cy.get("[data-cy=collection-info-panel")
272                     .should("contain", this.testCollection.name)
273                     .and("contain", "Color: Magenta")
274                     .and("contain", "Size: S");
275             });
276     });
277
278     it("shows collection by URL", function () {
279         cy.loginAs(activeUser);
280         [true, false].map(function (isWritable) {
281             // Using different file names to avoid test flakyness: the second iteration
282             // on this loop may pass an assertion from the first iteration by looking
283             // for the same file name.
284             const fileName = isWritable ? "bar" : "foo";
285             const subDirName = "subdir";
286             cy.createGroup(adminUser.token, {
287                 name: "Shared project",
288                 group_class: "project",
289             })
290                 .as("sharedGroup")
291                 .then(function () {
292                     // Creates the collection using the admin token so we can set up
293                     // a bogus manifest text without block signatures.
294                     cy.doRequest("GET", "/arvados/v1/config", null, null)
295                         .its("body")
296                         .should(clusterConfig => {
297                             expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", true);
298                             expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
299                             expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
300                             const inlineUrl =
301                                 clusterConfig.Services.WebDAV.ExternalURL !== ""
302                                     ? clusterConfig.Services.WebDAV.ExternalURL
303                                     : clusterConfig.Services.WebDAVDownload.ExternalURL;
304                             expect(inlineUrl).to.not.contain("*");
305                         })
306                         .createCollection(adminUser.token, {
307                             name: "Test collection",
308                             owner_uuid: this.sharedGroup.uuid,
309                             properties: { someKey: "someValue" },
310                             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
311                         })
312                         .as("testCollection")
313                         .then(function () {
314                             // Share the group with active user.
315                             cy.createLink(adminUser.token, {
316                                 name: isWritable ? "can_write" : "can_read",
317                                 link_class: "permission",
318                                 head_uuid: this.sharedGroup.uuid,
319                                 tail_uuid: activeUser.user.uuid,
320                             });
321                             cy.goToPath(`/collections/${this.testCollection.uuid}`);
322
323                             // Check that name & uuid are correct.
324                             cy.get("[data-cy=collection-info-panel]")
325                                 .should("contain", this.testCollection.name)
326                                 .and("contain", this.testCollection.uuid)
327                                 .and("not.contain", "This is an old version");
328                             // Check for the read-only icon
329                             cy.get("[data-cy=read-only-icon]").should(`${isWritable ? "not." : ""}exist`);
330                             // Check that both read and write operations are available on
331                             // the 'More options' menu.
332                             cy.get("[data-cy=collection-panel-options-btn]").click();
333                             cy.get("[data-cy=context-menu]")
334                                 .should("contain", "Add to favorites")
335                                 .and(`${isWritable ? "" : "not."}contain`, "Edit collection");
336                             cy.get("body").click(); // Collapse the menu avoiding details panel expansion
337                             cy.get("[data-cy=collection-info-panel]")
338                                 .should("contain", "someKey: someValue")
339                                 .and("not.contain", "anotherKey: anotherValue");
340                             // Check that the file listing show both read & write operations
341                             cy.waitForDom()
342                                 .get("[data-cy=collection-files-panel]")
343                                 .within(() => {
344                                     cy.get("[data-cy=collection-files-right-panel]", { timeout: 5000 }).should("contain", fileName);
345                                     if (isWritable) {
346                                         cy.get("[data-cy=upload-button]").should(`${isWritable ? "" : "not."}contain`, "Upload data");
347                                     }
348                                 });
349                             // Test context menus
350                             cy.get("[data-cy=collection-files-panel]").contains(fileName).rightclick();
351                             cy.get("[data-cy=context-menu]")
352                                 .should("contain", "Download")
353                                 .and("contain", "Open in new tab")
354                                 .and("contain", "Copy to clipboard")
355                                 .and(`${isWritable ? "" : "not."}contain`, "Rename")
356                                 .and(`${isWritable ? "" : "not."}contain`, "Remove");
357                             cy.get("body").click(); // Collapse the menu
358                             cy.get("[data-cy=collection-files-panel]").contains(subDirName).rightclick();
359                             cy.get("[data-cy=context-menu]")
360                                 .should("not.contain", "Download")
361                                 .and("contain", "Open in new tab")
362                                 .and("contain", "Copy to clipboard")
363                                 .and(`${isWritable ? "" : "not."}contain`, "Rename")
364                                 .and(`${isWritable ? "" : "not."}contain`, "Remove");
365                             cy.get("body").click(); // Collapse the menu
366                             // File/dir item 'more options' button
367                             cy.get("[data-cy=file-item-options-btn").first().click();
368                             cy.get("[data-cy=context-menu]").should(`${isWritable ? "" : "not."}contain`, "Remove");
369                             cy.get("body").click(); // Collapse the menu
370                             // Hamburger 'more options' menu button
371                             cy.get("[data-cy=collection-files-panel-options-btn]").click();
372                             cy.get("[data-cy=context-menu]").should("contain", "Select all").click();
373                             cy.get("[data-cy=collection-files-panel-options-btn]").click();
374                             cy.get("[data-cy=context-menu]").should(`${isWritable ? "" : "not."}contain`, "Remove selected");
375                             cy.get("body").click(); // Collapse the menu
376                         });
377                 });
378         });
379     });
380
381     it("renames a file using valid names", function () {
382         function eachPair(lst, func) {
383             for (var i = 0; i < lst.length - 1; i++) {
384                 func(lst[i], lst[i + 1]);
385             }
386         }
387         // Creates the collection using the admin token so we can set up
388         // a bogus manifest text without block signatures.
389         cy.createCollection(adminUser.token, {
390             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
391             owner_uuid: activeUser.user.uuid,
392             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
393         })
394             .as("testCollection")
395             .then(function () {
396                 cy.loginAs(activeUser);
397                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
398
399                 const names = [
400                     "bar", // initial name already set
401                     "&",
402                     "foo",
403                     "&amp;",
404                     "I ❤️ ⛵️",
405                     "...",
406                     "#..",
407                     "some name with whitespaces",
408                     "some name with #2",
409                     "is this name legal? I hope it is",
410                     "some_file.pdf#",
411                     "some_file.pdf?",
412                     "?some_file.pdf",
413                     "some%file.pdf",
414                     "some%2Ffile.pdf",
415                     "some%22file.pdf",
416                     "some%20file.pdf",
417                     "G%C3%BCnter's%20file.pdf",
418                     "table%&?*2",
419                     "bar", // make sure we can go back to the original name as a last step
420                 ];
421                 cy.intercept({ method: "PUT", url: "**/arvados/v1/collections/*" }).as("renameRequest");
422                 eachPair(names, (from, to) => {
423                     cy.waitForDom().get("[data-cy=collection-files-panel]").contains(`${from}`).rightclick();
424                     cy.get("[data-cy=context-menu]").contains("Rename").click();
425                     cy.get("[data-cy=form-dialog]")
426                         .should("contain", "Rename")
427                         .within(() => {
428                             cy.get("input").type("{selectall}{backspace}").type(to, { parseSpecialCharSequences: false });
429                         });
430                     cy.get("[data-cy=form-submit-btn]").click();
431                     cy.wait("@renameRequest");
432                     cy.get("[data-cy=collection-files-panel]").should("not.contain", `${from}`).and("contain", `${to}`);
433                 });
434             });
435     });
436
437     it("renames a file to a different directory", function () {
438         // Creates the collection using the admin token so we can set up
439         // a bogus manifest text without block signatures.
440         cy.createCollection(adminUser.token, {
441             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
442             owner_uuid: activeUser.user.uuid,
443             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
444         })
445             .as("testCollection")
446             .then(function () {
447                 cy.loginAs(activeUser);
448                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
449
450                 ["subdir", "G%C3%BCnter's%20file", "table%&?*2"].forEach(subdir => {
451                     cy.waitForDom().get("[data-cy=collection-files-panel]").contains("bar").rightclick();
452                     cy.get("[data-cy=context-menu]").contains("Rename").click();
453                     cy.get("[data-cy=form-dialog]")
454                         .should("contain", "Rename")
455                         .within(() => {
456                             cy.get("input").type(`{selectall}{backspace}${subdir}/foo`);
457                         });
458                     cy.get("[data-cy=form-submit-btn]").click();
459                     cy.get("[data-cy=collection-files-panel]").should("not.contain", "bar").and("contain", subdir);
460                     cy.get("[data-cy=collection-files-panel]").contains(subdir).click();
461
462                     // Rename 'subdir/foo' to 'bar'
463                     cy.wait(1000);
464                     cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
465                     cy.get("[data-cy=context-menu]").contains("Rename").click();
466                     cy.get("[data-cy=form-dialog]")
467                         .should("contain", "Rename")
468                         .within(() => {
469                             cy.get("input").should("have.value", `${subdir}/foo`).type(`{selectall}{backspace}bar`);
470                         });
471                     cy.get("[data-cy=form-submit-btn]").click();
472
473                     // need to wait for dialog to dismiss
474                     cy.get("[data-cy=form-dialog]").should("not.exist");
475
476                     cy.waitForDom().get("[data-cy=collection-files-panel]").contains("Home").click();
477
478                     cy.wait(2000);
479                     cy.get("[data-cy=collection-files-panel]")
480                         .should("contain", subdir) // empty dir kept
481                         .and("contain", "bar");
482
483                     cy.get("[data-cy=collection-files-panel]").contains(subdir).rightclick();
484                     cy.get("[data-cy=context-menu]").contains("Remove").click();
485                     cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
486                     cy.get("[data-cy=form-dialog]").should("not.exist");
487                 });
488             });
489     });
490
491     it("shows collection owner", () => {
492         cy.createCollection(adminUser.token, {
493             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
494             owner_uuid: activeUser.user.uuid,
495             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
496         })
497             .as("testCollection")
498             .then(testCollection => {
499                 cy.loginAs(activeUser);
500                 cy.goToPath(`/collections/${testCollection.uuid}`);
501                 cy.wait(5000);
502                 cy.get("[data-cy=collection-info-panel]").contains(`Collection User`);
503             });
504     });
505
506     it("tries to rename a file with illegal names", function () {
507         // Creates the collection using the admin token so we can set up
508         // a bogus manifest text without block signatures.
509         cy.createCollection(adminUser.token, {
510             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
511             owner_uuid: activeUser.user.uuid,
512             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
513         })
514             .as("testCollection")
515             .then(function () {
516                 cy.loginAs(activeUser);
517                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
518
519                 const illegalNamesFromUI = [
520                     [".", "Name cannot be '.' or '..'"],
521                     ["..", "Name cannot be '.' or '..'"],
522                     ["", "This field is required"],
523                     [" ", "Leading/trailing whitespaces not allowed"],
524                     [" foo", "Leading/trailing whitespaces not allowed"],
525                     ["foo ", "Leading/trailing whitespaces not allowed"],
526                     ["//foo", "Empty dir name not allowed"],
527                 ];
528                 illegalNamesFromUI.forEach(([name, errMsg]) => {
529                     cy.get("[data-cy=collection-files-panel]").contains("bar").rightclick();
530                     cy.get("[data-cy=context-menu]").contains("Rename").click();
531                     cy.get("[data-cy=form-dialog]")
532                         .should("contain", "Rename")
533                         .within(() => {
534                             cy.get("input").type(`{selectall}{backspace}${name}`);
535                         });
536                     cy.get("[data-cy=form-dialog]")
537                         .should("contain", "Rename")
538                         .within(() => {
539                             cy.contains(`${errMsg}`);
540                         });
541                     cy.get("[data-cy=form-cancel-btn]").click();
542                 });
543             });
544     });
545
546     it("can correctly display old versions", function () {
547         const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
548         let colUuid = "";
549         let oldVersionUuid = "";
550         // Make sure no other collections with this name exist
551         cy.doRequest("GET", "/arvados/v1/collections", null, {
552             filters: `[["name", "=", "${colName}"]]`,
553             include_old_versions: true,
554         })
555             .its("body.items")
556             .as("collections")
557             .then(function () {
558                 expect(this.collections).to.be.empty;
559             });
560         // Creates the collection using the admin token so we can set up
561         // a bogus manifest text without block signatures.
562         cy.createCollection(adminUser.token, {
563             name: colName,
564             owner_uuid: activeUser.user.uuid,
565             preserve_version: true,
566             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
567         })
568             .as("originalVersion")
569             .then(function () {
570                 // Change the file name to create a new version.
571                 cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
572                     manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n",
573                 });
574                 colUuid = this.originalVersion.uuid;
575             });
576         // Confirm that there are 2 versions of the collection
577         cy.doRequest("GET", "/arvados/v1/collections", null, {
578             filters: `[["name", "=", "${colName}"]]`,
579             include_old_versions: true,
580         })
581             .its("body.items")
582             .as("collections")
583             .then(function () {
584                 expect(this.collections).to.have.lengthOf(2);
585                 this.collections.map(function (aCollection) {
586                     expect(aCollection.current_version_uuid).to.equal(colUuid);
587                     if (aCollection.uuid !== aCollection.current_version_uuid) {
588                         oldVersionUuid = aCollection.uuid;
589                     }
590                 });
591                 // Check the old version displays as what it is.
592                 cy.loginAs(activeUser);
593                 cy.goToPath(`/collections/${oldVersionUuid}`);
594
595                 cy.get("[data-cy=collection-info-panel]").should("contain", "This is an old version");
596                 cy.get("[data-cy=read-only-icon]").should("exist");
597                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
598                 cy.get("[data-cy=collection-files-panel]").should("contain", "bar");
599             });
600     });
601
602     it("views & edits storage classes data", function () {
603         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
604         cy.createCollection(adminUser.token, {
605             name: colName,
606             owner_uuid: activeUser.user.uuid,
607             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
608         })
609             .as("collection")
610             .then(function () {
611                 expect(this.collection.storage_classes_desired).to.deep.equal(["default"]);
612
613                 cy.loginAs(activeUser);
614                 cy.goToPath(`/collections/${this.collection.uuid}`);
615
616                 // Initial check: it should show the 'default' storage class
617                 cy.get("[data-cy=collection-info-panel]")
618                     .should("contain", "Storage classes")
619                     .and("contain", "default")
620                     .and("not.contain", "foo")
621                     .and("not.contain", "bar");
622                 // Edit collection: add storage class 'foo'
623                 cy.get("[data-cy=collection-panel-options-btn]").click();
624                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
625                 cy.get("[data-cy=form-dialog]")
626                     .should("contain", "Edit Collection")
627                     .and("contain", "Storage classes")
628                     .and("contain", "default")
629                     .and("contain", "foo")
630                     .and("contain", "bar")
631                     .within(() => {
632                         cy.get("[data-cy=checkbox-foo]").click();
633                     });
634                 cy.get("[data-cy=form-submit-btn]").click();
635                 cy.get("[data-cy=collection-info-panel]").should("contain", "default").and("contain", "foo").and("not.contain", "bar");
636                 cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
637                     .its("body")
638                     .as("updatedCollection")
639                     .then(function () {
640                         expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["default", "foo"]);
641                     });
642                 // Edit collection: remove storage class 'default'
643                 cy.get("[data-cy=collection-panel-options-btn]").click();
644                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
645                 cy.get("[data-cy=form-dialog]")
646                     .should("contain", "Edit Collection")
647                     .and("contain", "Storage classes")
648                     .and("contain", "default")
649                     .and("contain", "foo")
650                     .and("contain", "bar")
651                     .within(() => {
652                         cy.get("[data-cy=checkbox-default]").click();
653                     });
654                 cy.get("[data-cy=form-submit-btn]").click();
655                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "default").and("contain", "foo").and("not.contain", "bar");
656                 cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
657                     .its("body")
658                     .as("updatedCollection")
659                     .then(function () {
660                         expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["foo"]);
661                     });
662             });
663     });
664
665     it("moves a collection to a different project", function () {
666         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
667         const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
668         const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
669
670         cy.createCollection(adminUser.token, {
671             name: collName,
672             owner_uuid: activeUser.user.uuid,
673             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
674         }).as("testCollection");
675         cy.createGroup(adminUser.token, {
676             name: projName,
677             group_class: "project",
678             owner_uuid: activeUser.user.uuid,
679         }).as("testProject");
680
681         cy.getAll("@testCollection", "@testProject").then(function ([testCollection, testProject]) {
682             cy.loginAs(activeUser);
683             cy.goToPath(`/collections/${testCollection.uuid}`);
684             cy.get("[data-cy=collection-files-panel]").should("contain", fileName);
685             cy.get("[data-cy=collection-info-panel]").should("not.contain", projName).and("not.contain", testProject.uuid);
686             cy.get("[data-cy=collection-panel-options-btn]").click();
687             cy.get("[data-cy=context-menu]").contains("Move to").click();
688             cy.get("[data-cy=form-dialog]")
689                 .should("contain", "Move to")
690                 .within(() => {
691                     // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
692                     cy.get("[data-cy=projects-tree-home-tree-picker]")
693                         .find("i")
694                         .then(el => el.click());
695                     cy.get("[data-cy=projects-tree-home-tree-picker]").contains(projName).click();
696                 });
697             cy.get("[data-cy=form-submit-btn]").click();
698             cy.get("[data-cy=snackbar]").contains("Collection has been moved");
699             cy.get("[data-cy=collection-info-panel]").contains(projName).and("contain", testProject.uuid);
700             // Double check that the collection is in the project
701             cy.goToPath(`/projects/${testProject.uuid}`);
702             cy.waitForDom().get("[data-cy=project-panel]").should("contain", collName);
703         });
704     });
705
706     it("automatically updates the collection UI contents without using the Refresh button", function () {
707         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
708
709         cy.createCollection(adminUser.token, {
710             name: collName,
711             owner_uuid: activeUser.user.uuid,
712         }).as("testCollection");
713
714         cy.getAll("@testCollection").then(function ([testCollection]) {
715             cy.loginAs(activeUser);
716
717             const files = ["foobar", "anotherFile", "", "finalName"];
718
719             cy.goToPath(`/collections/${testCollection.uuid}`);
720             cy.get("[data-cy=collection-files-panel]").should("contain", "This collection is empty");
721             cy.get("[data-cy=collection-files-panel]").should("not.contain", files[0]);
722             cy.get("[data-cy=collection-info-panel]").should("contain", collName);
723
724             files.map((fileName, i, files) => {
725                 cy.updateCollection(adminUser.token, testCollection.uuid, {
726                     name: `${collName + " updated"}`,
727                     manifest_text: fileName ? `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n` : "",
728                 }).as("updatedCollection");
729                 cy.getAll("@updatedCollection").then(function ([updatedCollection]) {
730                     expect(updatedCollection.name).to.equal(`${collName + " updated"}`);
731                     cy.get("[data-cy=collection-info-panel]").should("contain", updatedCollection.name);
732                     fileName
733                         ? cy.get("[data-cy=collection-files-panel]").should("contain", fileName)
734                         : cy.get("[data-cy=collection-files-panel]").should("not.contain", files[i - 1]);
735                 });
736             });
737         });
738     });
739
740     it("makes a copy of an existing collection", function () {
741         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
742         const copyName = `Copy of: ${collName}`;
743
744         cy.createCollection(adminUser.token, {
745             name: collName,
746             owner_uuid: activeUser.user.uuid,
747             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
748         })
749             .as("collection")
750             .then(function () {
751                 cy.loginAs(activeUser);
752                 cy.goToPath(`/collections/${this.collection.uuid}`);
753                 cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
754                 cy.get("[data-cy=collection-panel-options-btn]").click();
755                 cy.get("[data-cy=context-menu]").contains("Make a copy").click();
756                 cy.get("[data-cy=form-dialog]")
757                     .should("contain", "Make a copy")
758                     .within(() => {
759                         cy.get("[data-cy=projects-tree-home-tree-picker]").contains("Projects").click();
760                         cy.get("[data-cy=form-submit-btn]").click();
761                     });
762                 cy.get("[data-cy=snackbar]").contains("Collection has been copied.");
763                 cy.get("[data-cy=snackbar-goto-action]").click();
764                 cy.get("[data-cy=project-panel]").contains(copyName).click();
765                 cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
766             });
767     });
768
769     it("uses the collection version browser to view a previous version", function () {
770         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
771
772         // Creates the collection using the admin token so we can set up
773         // a bogus manifest text without block signatures.
774         cy.createCollection(adminUser.token, {
775             name: colName,
776             owner_uuid: activeUser.user.uuid,
777             preserve_version: true,
778             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
779         })
780             .as("collection")
781             .then(function () {
782                 // Visit collection, check basic information
783                 cy.loginAs(activeUser);
784                 cy.goToPath(`/collections/${this.collection.uuid}`);
785
786                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
787                 cy.get("[data-cy=read-only-icon]").should("not.exist");
788                 cy.get("[data-cy=collection-version-number]").should("contain", "1");
789                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
790                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
791
792                 // Modify collection, expect version number change
793                 cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
794                 cy.get("[data-cy=context-menu]").contains("Remove").click();
795                 cy.get("[data-cy=confirmation-dialog]").should("contain", "Removing file");
796                 cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
797                 cy.get("[data-cy=collection-version-number]").should("contain", "2");
798                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
799
800                 // Click on version number, check version browser. Click on past version.
801                 cy.get("[data-cy=collection-version-browser]").should("not.exist");
802                 cy.get("[data-cy=collection-version-number]").contains("2").click();
803                 cy.get("[data-cy=collection-version-browser]")
804                     .should("contain", "Nr")
805                     .and("contain", "Size")
806                     .and("contain", "Date")
807                     .within(() => {
808                         // Version 1: 6 bytes in size
809                         cy.get("[data-cy=collection-version-browser-select-1]")
810                             .should("contain", "1")
811                             .and("contain", "6 B")
812                             .and("contain", adminUser.user.full_name);
813                         // Version 2: 3 bytes in size (one file removed)
814                         cy.get("[data-cy=collection-version-browser-select-2]")
815                             .should("contain", "2")
816                             .and("contain", "3 B")
817                             .and("contain", activeUser.user.full_name);
818                         cy.get("[data-cy=collection-version-browser-select-3]").should("not.exist");
819                         cy.get("[data-cy=collection-version-browser-select-1]").click();
820                     });
821                 cy.get("[data-cy=collection-info-panel]").should("contain", "This is an old version");
822                 cy.get("[data-cy=read-only-icon]").should("exist");
823                 cy.get("[data-cy=collection-version-number]").should("contain", "1");
824                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
825                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
826
827                 // Check that only old collection action are available on context menu
828                 cy.get("[data-cy=collection-panel-options-btn]").click();
829                 cy.get("[data-cy=context-menu]").should("contain", "Restore version").and("not.contain", "Add to favorites");
830                 cy.get("body").click(); // Collapse the menu avoiding details panel expansion
831
832                 // Click on "head version" link, confirm that it's the latest version.
833                 cy.get("[data-cy=collection-info-panel]").contains("head version").click();
834                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
835                 cy.get("[data-cy=read-only-icon]").should("not.exist");
836                 cy.get("[data-cy=collection-version-number]").should("contain", "2");
837                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
838                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
839
840                 // Check that old collection action isn't available on context menu
841                 cy.get("[data-cy=collection-panel-options-btn]").click();
842                 cy.get("[data-cy=context-menu]").should("not.contain", "Restore version");
843                 cy.get("body").click(); // Collapse the menu avoiding details panel expansion
844
845                 // Make another change, confirm new version.
846                 cy.get("[data-cy=collection-panel-options-btn]").click();
847                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
848                 cy.get("[data-cy=form-dialog]")
849                     .should("contain", "Edit Collection")
850                     .within(() => {
851                         // appends some text
852                         cy.get("input").first().type(" renamed");
853                     });
854                 cy.get("[data-cy=form-submit-btn]").click();
855                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
856                 cy.get("[data-cy=read-only-icon]").should("not.exist");
857                 cy.get("[data-cy=collection-version-number]").should("contain", "3");
858                 cy.get("[data-cy=collection-info-panel]").should("contain", colName + " renamed");
859                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
860                 cy.get("[data-cy=collection-version-browser-select-3]").should("contain", "3").and("contain", "3 B");
861
862                 // Check context menus on version browser
863                 cy.waitForDom();
864                 cy.get("[data-cy=collection-version-browser-select-3]").rightclick();
865                 cy.get("[data-cy=context-menu]")
866                     .should("contain", "Add to favorites")
867                     .and("contain", "Make a copy")
868                     .and("contain", "Edit collection");
869                 cy.get("body").click();
870                 // (and now an old version...)
871                 cy.get("[data-cy=collection-version-browser-select-1]").rightclick();
872                 cy.get("[data-cy=context-menu]")
873                     .should("not.contain", "Add to favorites")
874                     .and("contain", "Make a copy")
875                     .and("not.contain", "Edit collection");
876                 cy.get("body").click();
877
878                 // Restore first version
879                 cy.get("[data-cy=collection-version-browser]").within(() => {
880                     cy.get("[data-cy=collection-version-browser-select-1]").click();
881                 });
882                 cy.get("[data-cy=collection-panel-options-btn]").click();
883                 cy.get("[data-cy=context-menu]").contains("Restore version").click();
884                 cy.get("[data-cy=confirmation-dialog]").should("contain", "Restore version");
885                 cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
886                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
887                 cy.get("[data-cy=collection-version-number]").should("contain", "4");
888                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
889                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
890             });
891     });
892
893     it("copies selected files into new collection", () => {
894         cy.createCollection(adminUser.token, {
895             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
896             owner_uuid: activeUser.user.uuid,
897             preserve_version: true,
898             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
899         })
900             .as("collection")
901             .then(function () {
902                 // Visit collection, check basic information
903                 cy.loginAs(activeUser);
904                 cy.goToPath(`/collections/${this.collection.uuid}`);
905
906                 cy.get("[data-cy=collection-files-panel]").within(() => {
907                     cy.get("input[type=checkbox]").first().click();
908                 });
909
910                 cy.get("[data-cy=collection-files-panel-options-btn]").click();
911                 cy.get("[data-cy=context-menu]").contains("Copy selected into new collection").click();
912
913                 cy.get("[data-cy=form-dialog]").contains("Projects").click();
914
915                 cy.get("[data-cy=form-submit-btn]").click();
916
917                 cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
918
919                 cy.waitForDom().get("main").contains(`Files extracted from: ${this.collection.name}`).click();
920                 cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
921             });
922     });
923
924     it("copies selected files into existing collection", () => {
925         cy.createCollection(adminUser.token, {
926             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
927             owner_uuid: activeUser.user.uuid,
928             preserve_version: true,
929             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
930         }).as("sourceCollection");
931
932         cy.createCollection(adminUser.token, {
933             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
934             owner_uuid: activeUser.user.uuid,
935             preserve_version: true,
936             manifest_text: "",
937         }).as("destinationCollection");
938
939         cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
940             // Visit collection, check basic information
941             cy.loginAs(activeUser);
942             cy.goToPath(`/collections/${sourceCollection.uuid}`);
943
944             cy.get("[data-cy=collection-files-panel]").within(() => {
945                 cy.get("input[type=checkbox]").first().click();
946             });
947
948             cy.get("[data-cy=collection-files-panel-options-btn]").click();
949             cy.get("[data-cy=context-menu]").contains("Copy selected into existing collection").click();
950
951             cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
952
953             cy.get("[data-cy=form-submit-btn]").click();
954             cy.wait(2000);
955
956             cy.goToPath(`/collections/${destinationCollection.uuid}`);
957
958             cy.get("main").contains(destinationCollection.name).should("exist");
959             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
960         });
961     });
962
963     it("copies selected files into separate collections", () => {
964         cy.createCollection(adminUser.token, {
965             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
966             owner_uuid: activeUser.user.uuid,
967             preserve_version: true,
968             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
969         }).as("sourceCollection");
970
971         cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
972             // Visit collection, check basic information
973             cy.loginAs(activeUser);
974             cy.goToPath(`/collections/${sourceCollection.uuid}`);
975
976             // Select both files
977             cy.waitForDom()
978                 .get("[data-cy=collection-files-panel]")
979                 .within(() => {
980                     cy.get("input[type=checkbox]").first().click();
981                     cy.get("input[type=checkbox]").last().click();
982                 });
983
984             // Copy to separate collections
985             cy.get("[data-cy=collection-files-panel-options-btn]").click();
986             cy.get("[data-cy=context-menu]").contains("Copy selected into separate collections").click();
987             cy.get("[data-cy=form-dialog]").contains("Projects").click();
988             cy.get("[data-cy=form-submit-btn]").click();
989
990             // Verify created collections
991             cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
992             cy.get("main").contains(`File copied from collection ${sourceCollection.name}/foo`).click();
993             cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
994             cy.get(".layout-pane-primary").contains("Projects").click();
995             cy.get("main").contains(`File copied from collection ${sourceCollection.name}/bar`).click();
996             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
997
998             // Verify separate collection menu items not present when single file selected
999             // Wait for dom for collection to re-render
1000             cy.waitForDom()
1001                 .get("[data-cy=collection-files-panel]")
1002                 .within(() => {
1003                     cy.get("input[type=checkbox]").first().click();
1004                 });
1005             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1006             cy.get("[data-cy=context-menu]").should("not.contain", "Copy selected into separate collections");
1007             cy.get("[data-cy=context-menu]").should("not.contain", "Move selected into separate collections");
1008         });
1009     });
1010
1011     it("moves selected files into new collection", () => {
1012         cy.createCollection(adminUser.token, {
1013             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1014             owner_uuid: activeUser.user.uuid,
1015             preserve_version: true,
1016             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1017         })
1018             .as("collection")
1019             .then(function () {
1020                 // Visit collection, check basic information
1021                 cy.loginAs(activeUser);
1022                 cy.goToPath(`/collections/${this.collection.uuid}`);
1023
1024                 cy.get("[data-cy=collection-files-panel]").within(() => {
1025                     cy.get("input[type=checkbox]").first().click();
1026                 });
1027
1028                 cy.get("[data-cy=collection-files-panel-options-btn]").click();
1029                 cy.get("[data-cy=context-menu]").contains("Move selected into new collection").click();
1030
1031                 cy.get("[data-cy=form-dialog]").contains("Projects").click();
1032
1033                 cy.get("[data-cy=form-submit-btn]").click();
1034
1035                 cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
1036
1037                 cy.get("main").contains(`Files moved from: ${this.collection.name}`).click();
1038                 cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1039             });
1040     });
1041
1042     it("moves selected files into existing collection", () => {
1043         cy.createCollection(adminUser.token, {
1044             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1045             owner_uuid: activeUser.user.uuid,
1046             preserve_version: true,
1047             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1048         }).as("sourceCollection");
1049
1050         cy.createCollection(adminUser.token, {
1051             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
1052             owner_uuid: activeUser.user.uuid,
1053             preserve_version: true,
1054             manifest_text: "",
1055         }).as("destinationCollection");
1056
1057         cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
1058             // Visit collection, check basic information
1059             cy.loginAs(activeUser);
1060             cy.goToPath(`/collections/${sourceCollection.uuid}`);
1061
1062             cy.get("[data-cy=collection-files-panel]").within(() => {
1063                 cy.get("input[type=checkbox]").first().click();
1064             });
1065
1066             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1067             cy.get("[data-cy=context-menu]").contains("Move selected into existing collection").click();
1068
1069             cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
1070
1071             cy.get("[data-cy=form-submit-btn]").click();
1072             cy.wait(2000);
1073
1074             cy.goToPath(`/collections/${destinationCollection.uuid}`);
1075
1076             cy.get("main").contains(destinationCollection.name).should("exist");
1077             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1078         });
1079     });
1080
1081     it("moves selected files into separate collections", () => {
1082         cy.createCollection(adminUser.token, {
1083             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1084             owner_uuid: activeUser.user.uuid,
1085             preserve_version: true,
1086             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1087         }).as("sourceCollection");
1088
1089         cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
1090             // Visit collection, check basic information
1091             cy.loginAs(activeUser);
1092             cy.goToPath(`/collections/${sourceCollection.uuid}`);
1093
1094             // Select both files
1095             cy.get("[data-cy=collection-files-panel]").within(() => {
1096                 cy.get("input[type=checkbox]").first().click();
1097                 cy.get("input[type=checkbox]").last().click();
1098             });
1099
1100             // Copy to separate collections
1101             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1102             cy.get("[data-cy=context-menu]").contains("Move selected into separate collections").click();
1103             cy.get("[data-cy=form-dialog]").contains("Projects").click();
1104             cy.get("[data-cy=form-submit-btn]").click();
1105
1106             // Verify created collections
1107             cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
1108             cy.get("main").contains(`File moved from collection ${sourceCollection.name}/foo`).click();
1109             cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
1110             cy.get(".layout-pane-primary").contains("Projects").click();
1111             cy.get("main").contains(`File moved from collection ${sourceCollection.name}/bar`).click();
1112             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1113         });
1114     });
1115
1116     it("creates new collection with properties on home project", function () {
1117         cy.loginAs(activeUser);
1118         cy.goToPath(`/projects/${activeUser.user.uuid}`);
1119         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
1120         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
1121         // Create new collection
1122         cy.get("[data-cy=side-panel-button]").click();
1123         cy.get("[data-cy=side-panel-new-collection]").click();
1124         // Name between brackets tests bugfix #17582
1125         const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
1126
1127         // Select a storage class.
1128         cy.get("[data-cy=form-dialog]")
1129             .should("contain", "New collection")
1130             .and("contain", "Storage classes")
1131             .and("contain", "default")
1132             .and("contain", "foo")
1133             .and("contain", "bar")
1134             .within(() => {
1135                 cy.get("[data-cy=parent-field]").within(() => {
1136                     cy.get("input").should("have.value", "Home project");
1137                 });
1138                 cy.get("[data-cy=name-field]").within(() => {
1139                     cy.get("input").type(collName);
1140                 });
1141                 cy.get("[data-cy=checkbox-foo]").click();
1142             });
1143
1144         // Add a property.
1145         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
1146         cy.get("[data-cy=form-dialog]").should("not.contain", "Color: Magenta");
1147         cy.get("[data-cy=resource-properties-form]").within(() => {
1148             cy.get("[data-cy=property-field-key]").within(() => {
1149                 cy.get("input").type("Color");
1150             });
1151             cy.get("[data-cy=property-field-value]").within(() => {
1152                 cy.get("input").type("Magenta");
1153             });
1154             cy.root().submit();
1155         });
1156         // Confirm proper vocabulary labels are displayed on the UI.
1157         cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
1158
1159         // Value field should not complain about being required just after
1160         // adding a new property. See #19732
1161         cy.get("[data-cy=form-dialog]").should("not.contain", "This field is required");
1162
1163         cy.get("[data-cy=form-submit-btn]").click();
1164         // Confirm that the user was taken to the newly created collection
1165         cy.get("[data-cy=form-dialog]").should("not.exist");
1166         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
1167         cy.get("[data-cy=breadcrumb-last]").should("contain", collName);
1168         cy.get("[data-cy=collection-info-panel]")
1169             .should("contain", "default")
1170             .and("contain", "foo")
1171             .and("contain", "Color: Magenta")
1172             .and("not.contain", "bar");
1173         // Confirm that the collection's properties has the real values.
1174         cy.doRequest("GET", "/arvados/v1/collections", null, {
1175             filters: `[["name", "=", "${collName}"]]`,
1176         })
1177             .its("body.items")
1178             .as("collections")
1179             .then(function () {
1180                 expect(this.collections).to.have.lengthOf(1);
1181                 expect(this.collections[0].properties).to.have.property("IDTAGCOLORS", "IDVALCOLORS3");
1182             });
1183     });
1184
1185     it("shows responsible person for collection if available", () => {
1186         cy.createCollection(adminUser.token, {
1187             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1188             owner_uuid: activeUser.user.uuid,
1189             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1190         }).as("testCollection1");
1191
1192         cy.createCollection(adminUser.token, {
1193             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1194             owner_uuid: adminUser.user.uuid,
1195             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1196         })
1197             .as("testCollection2")
1198             .then(function (testCollection2) {
1199                 cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, "can_write");
1200             });
1201
1202         cy.getAll("@testCollection1", "@testCollection2").then(function ([testCollection1, testCollection2]) {
1203             cy.loginAs(activeUser);
1204
1205             cy.goToPath(`/collections/${testCollection1.uuid}`);
1206             cy.get("[data-cy=responsible-person-wrapper]").contains(activeUser.user.uuid);
1207
1208             cy.goToPath(`/collections/${testCollection2.uuid}`);
1209             cy.get("[data-cy=responsible-person-wrapper]").contains(adminUser.user.uuid);
1210         });
1211     });
1212
1213     describe("file upload", () => {
1214         beforeEach(() => {
1215             cy.createCollection(adminUser.token, {
1216                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1217                 owner_uuid: activeUser.user.uuid,
1218                 manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1219             }).as("testCollection1");
1220         });
1221
1222         it("uploads a file and checks the collection UI to be fresh", () => {
1223             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1224                 cy.loginAs(activeUser);
1225                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1226                 cy.get("[data-cy=upload-button]").click();
1227                 cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
1228                 cy.get("[data-cy=collection-file-count]").should("contain", "2");
1229                 cy.fixture("files/5mb.bin", "base64").then(content => {
1230                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1231                     cy.get("[data-cy=form-submit-btn]").click();
1232                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1233                     cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
1234                     cy.get("[data-cy=collection-file-count]").should("contain", "3");
1235
1236                     cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
1237                     cy.get("[data-cy=upload-button]").click();
1238                     cy.fixture("files/5mb.bin", "base64").then(content => {
1239                         cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1240                         cy.get("[data-cy=form-submit-btn]").click();
1241                         cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1242                         // subdir gets unselected, I think this is a bug but
1243                         // for the time being let's just make sure the test works.
1244                         cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
1245                         cy.waitForDom().get("[data-cy=collection-files-right-panel]").contains("5mb_b.bin").should("exist");
1246                     });
1247                 });
1248             });
1249         });
1250
1251         it("allows to cancel running upload", () => {
1252             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1253                 cy.loginAs(activeUser);
1254
1255                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1256
1257                 cy.get("[data-cy=upload-button]").click();
1258
1259                 cy.fixture("files/5mb.bin", "base64").then(content => {
1260                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1261                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1262
1263                     cy.get("[data-cy=form-submit-btn]").click();
1264
1265                     cy.get("button").contains("Cancel").click();
1266
1267                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1268                 });
1269             });
1270         });
1271
1272         it("allows to cancel single file from the running upload", () => {
1273             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1274                 cy.loginAs(activeUser);
1275
1276                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1277
1278                 cy.get("[data-cy=upload-button]").click();
1279
1280                 cy.fixture("files/5mb.bin", "base64").then(content => {
1281                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1282                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1283
1284                     cy.get("[data-cy=form-submit-btn]").click();
1285
1286                     cy.get("button[aria-label=Remove]").eq(1).click();
1287
1288                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1289
1290                     cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
1291                 });
1292             });
1293         });
1294
1295         it("allows to cancel all files from the running upload", () => {
1296             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1297                 cy.loginAs(activeUser);
1298
1299                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1300
1301                 // Confirm initial collection state.
1302                 cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
1303                 cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
1304                 cy.get("[data-cy=collection-files-panel]").contains("5mb_b.bin").should("not.exist");
1305
1306                 cy.get("[data-cy=upload-button]").click();
1307
1308                 cy.fixture("files/5mb.bin", "base64").then(content => {
1309                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1310                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1311
1312                     cy.get("[data-cy=form-submit-btn]").click();
1313
1314                     cy.get("button[aria-label=Remove]").should("exist").click({ multiple: true});
1315
1316                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1317
1318                     // Confirm final collection state.
1319                     cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
1320                     // The following fails, but doesn't seem to happen
1321                     // in the real world. Maybe there's a race between
1322                     // the PUT request finishing and the 'Remove' button
1323                     // dissapearing, because sometimes just one of the 2
1324                     // files gets uploaded.
1325                     // Maybe this will be needed to simulate a slow network:
1326                     // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
1327                     // cy.get('[data-cy=collection-files-panel]')
1328                     //     .contains('5mb_a.bin').should('not.exist');
1329                     // cy.get('[data-cy=collection-files-panel]')
1330                     //     .contains('5mb_b.bin').should('not.exist');
1331                 });
1332             });
1333         });
1334     });
1335 });