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