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