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