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