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