Merge branch 'main' into 15768-multi-select-operations Arvados-DCO-1.1-Signed-off...
[arvados-workbench2.git] / 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("renames a file to a different directory", function () {
457         // Creates the collection using the admin token so we can set up
458         // a bogus manifest text without block signatures.
459         cy.createCollection(adminUser.token, {
460             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
461             owner_uuid: activeUser.user.uuid,
462             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
463         })
464             .as("testCollection")
465             .then(function () {
466                 cy.loginAs(activeUser);
467                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
468
469                 ["subdir", "G%C3%BCnter's%20file", "table%&?*2"].forEach(subdir => {
470                     cy.waitForDom().get("[data-cy=collection-files-panel]").contains("bar").rightclick();
471                     cy.get("[data-cy=context-menu]").contains("Rename").click();
472                     cy.get("[data-cy=form-dialog]")
473                         .should("contain", "Rename")
474                         .within(() => {
475                             cy.get("input").type(`{selectall}{backspace}${subdir}/foo`);
476                         });
477                     cy.get("[data-cy=form-submit-btn]").click();
478                     cy.get("[data-cy=collection-files-panel]").should("not.contain", "bar").and("contain", subdir);
479                     cy.get("[data-cy=collection-files-panel]").contains(subdir).click();
480
481                     // Rename 'subdir/foo' to 'bar'
482                     cy.wait(1000);
483                     cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
484                     cy.get("[data-cy=context-menu]").contains("Rename").click();
485                     cy.get("[data-cy=form-dialog]")
486                         .should("contain", "Rename")
487                         .within(() => {
488                             cy.get("input").should("have.value", `${subdir}/foo`).type(`{selectall}{backspace}bar`);
489                         });
490                     cy.get("[data-cy=form-submit-btn]").click();
491
492                     // need to wait for dialog to dismiss
493                     cy.get("[data-cy=form-dialog]").should("not.exist");
494
495                     cy.waitForDom().get("[data-cy=collection-files-panel]").contains("Home").click();
496
497                     cy.wait(2000);
498                     cy.get("[data-cy=collection-files-panel]")
499                         .should("contain", subdir) // empty dir kept
500                         .and("contain", "bar");
501
502                     cy.get("[data-cy=collection-files-panel]").contains(subdir).rightclick();
503                     cy.get("[data-cy=context-menu]").contains("Remove").click();
504                     cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
505                     cy.get("[data-cy=form-dialog]").should("not.exist");
506                 });
507             });
508     });
509
510     it("shows collection owner", () => {
511         cy.createCollection(adminUser.token, {
512             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
513             owner_uuid: activeUser.user.uuid,
514             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
515         })
516             .as("testCollection")
517             .then(testCollection => {
518                 cy.loginAs(activeUser);
519                 cy.goToPath(`/collections/${testCollection.uuid}`);
520                 cy.wait(5000);
521                 cy.get("[data-cy=collection-info-panel]").contains(`Collection User`);
522             });
523     });
524
525     it("tries to rename a file with illegal names", function () {
526         // Creates the collection using the admin token so we can set up
527         // a bogus manifest text without block signatures.
528         cy.createCollection(adminUser.token, {
529             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
530             owner_uuid: activeUser.user.uuid,
531             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
532         })
533             .as("testCollection")
534             .then(function () {
535                 cy.loginAs(activeUser);
536                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
537
538                 const illegalNamesFromUI = [
539                     [".", "Name cannot be '.' or '..'"],
540                     ["..", "Name cannot be '.' or '..'"],
541                     ["", "This field is required"],
542                     [" ", "Leading/trailing whitespaces not allowed"],
543                     [" foo", "Leading/trailing whitespaces not allowed"],
544                     ["foo ", "Leading/trailing whitespaces not allowed"],
545                     ["//foo", "Empty dir name not allowed"],
546                 ];
547                 illegalNamesFromUI.forEach(([name, errMsg]) => {
548                     cy.get("[data-cy=collection-files-panel]").contains("bar").rightclick();
549                     cy.get("[data-cy=context-menu]").contains("Rename").click();
550                     cy.get("[data-cy=form-dialog]")
551                         .should("contain", "Rename")
552                         .within(() => {
553                             cy.get("input").type(`{selectall}{backspace}${name}`);
554                         });
555                     cy.get("[data-cy=form-dialog]")
556                         .should("contain", "Rename")
557                         .within(() => {
558                             cy.contains(`${errMsg}`);
559                         });
560                     cy.get("[data-cy=form-cancel-btn]").click();
561                 });
562             });
563     });
564
565     it("can correctly display old versions", function () {
566         const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
567         let colUuid = "";
568         let oldVersionUuid = "";
569         // Make sure no other collections with this name exist
570         cy.doRequest("GET", "/arvados/v1/collections", null, {
571             filters: `[["name", "=", "${colName}"]]`,
572             include_old_versions: true,
573         })
574             .its("body.items")
575             .as("collections")
576             .then(function () {
577                 expect(this.collections).to.be.empty;
578             });
579         // Creates the collection using the admin token so we can set up
580         // a bogus manifest text without block signatures.
581         cy.createCollection(adminUser.token, {
582             name: colName,
583             owner_uuid: activeUser.user.uuid,
584             preserve_version: true,
585             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
586         })
587             .as("originalVersion")
588             .then(function () {
589                 // Change the file name to create a new version.
590                 cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
591                     manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n",
592                 });
593                 colUuid = this.originalVersion.uuid;
594             });
595         // Confirm that there are 2 versions of the collection
596         cy.doRequest("GET", "/arvados/v1/collections", null, {
597             filters: `[["name", "=", "${colName}"]]`,
598             include_old_versions: true,
599         })
600             .its("body.items")
601             .as("collections")
602             .then(function () {
603                 expect(this.collections).to.have.lengthOf(2);
604                 this.collections.map(function (aCollection) {
605                     expect(aCollection.current_version_uuid).to.equal(colUuid);
606                     if (aCollection.uuid !== aCollection.current_version_uuid) {
607                         oldVersionUuid = aCollection.uuid;
608                     }
609                 });
610                 // Check the old version displays as what it is.
611                 cy.loginAs(activeUser);
612                 cy.goToPath(`/collections/${oldVersionUuid}`);
613
614                 cy.get("[data-cy=collection-info-panel]").should("contain", "This is an old version");
615                 cy.get("[data-cy=read-only-icon]").should("exist");
616                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
617                 cy.get("[data-cy=collection-files-panel]").should("contain", "bar");
618             });
619     });
620
621     it("views & edits storage classes data", function () {
622         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
623         cy.createCollection(adminUser.token, {
624             name: colName,
625             owner_uuid: activeUser.user.uuid,
626             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
627         })
628             .as("collection")
629             .then(function () {
630                 expect(this.collection.storage_classes_desired).to.deep.equal(["default"]);
631
632                 cy.loginAs(activeUser);
633                 cy.goToPath(`/collections/${this.collection.uuid}`);
634
635                 // Initial check: it should show the 'default' storage class
636                 cy.get("[data-cy=collection-info-panel]")
637                     .should("contain", "Storage classes")
638                     .and("contain", "default")
639                     .and("not.contain", "foo")
640                     .and("not.contain", "bar");
641                 // Edit collection: add storage class 'foo'
642                 cy.get("[data-cy=collection-panel-options-btn]").click();
643                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
644                 cy.get("[data-cy=form-dialog]")
645                     .should("contain", "Edit Collection")
646                     .and("contain", "Storage classes")
647                     .and("contain", "default")
648                     .and("contain", "foo")
649                     .and("contain", "bar")
650                     .within(() => {
651                         cy.get("[data-cy=checkbox-foo]").click();
652                     });
653                 cy.get("[data-cy=form-submit-btn]").click();
654                 cy.get("[data-cy=collection-info-panel]").should("contain", "default").and("contain", "foo").and("not.contain", "bar");
655                 cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
656                     .its("body")
657                     .as("updatedCollection")
658                     .then(function () {
659                         expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["default", "foo"]);
660                     });
661                 // Edit collection: remove storage class 'default'
662                 cy.get("[data-cy=collection-panel-options-btn]").click();
663                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
664                 cy.get("[data-cy=form-dialog]")
665                     .should("contain", "Edit Collection")
666                     .and("contain", "Storage classes")
667                     .and("contain", "default")
668                     .and("contain", "foo")
669                     .and("contain", "bar")
670                     .within(() => {
671                         cy.get("[data-cy=checkbox-default]").click();
672                     });
673                 cy.get("[data-cy=form-submit-btn]").click();
674                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "default").and("contain", "foo").and("not.contain", "bar");
675                 cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
676                     .its("body")
677                     .as("updatedCollection")
678                     .then(function () {
679                         expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["foo"]);
680                     });
681             });
682     });
683
684     it("moves a collection to a different project", function () {
685         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
686         const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
687         const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
688
689         cy.createCollection(adminUser.token, {
690             name: collName,
691             owner_uuid: activeUser.user.uuid,
692             manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
693         }).as("testCollection");
694         cy.createGroup(adminUser.token, {
695             name: projName,
696             group_class: "project",
697             owner_uuid: activeUser.user.uuid,
698         }).as("testProject");
699
700         cy.getAll("@testCollection", "@testProject").then(function ([testCollection, testProject]) {
701             cy.loginAs(activeUser);
702             cy.goToPath(`/collections/${testCollection.uuid}`);
703             cy.get("[data-cy=collection-files-panel]").should("contain", fileName);
704             cy.get("[data-cy=collection-info-panel]").should("not.contain", projName).and("not.contain", testProject.uuid);
705             cy.get("[data-cy=collection-panel-options-btn]").click();
706             cy.get("[data-cy=context-menu]").contains("Move to").click();
707             cy.get("[data-cy=form-dialog]")
708                 .should("contain", "Move to")
709                 .within(() => {
710                     // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
711                     cy.get("[data-cy=projects-tree-home-tree-picker]")
712                         .find("i")
713                         .then(el => el.click());
714                     cy.get("[data-cy=projects-tree-home-tree-picker]").contains(projName).click();
715                 });
716             cy.get("[data-cy=form-submit-btn]").click();
717             cy.get("[data-cy=snackbar]").contains("Collection has been moved");
718             cy.get("[data-cy=collection-info-panel]").contains(projName).and("contain", testProject.uuid);
719             // Double check that the collection is in the project
720             cy.goToPath(`/projects/${testProject.uuid}`);
721             cy.waitForDom().get("[data-cy=project-panel]").should("contain", collName);
722         });
723     });
724
725     it("automatically updates the collection UI contents without using the Refresh button", function () {
726         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
727
728         cy.createCollection(adminUser.token, {
729             name: collName,
730             owner_uuid: activeUser.user.uuid,
731         }).as("testCollection");
732
733         cy.getAll("@testCollection").then(function ([testCollection]) {
734             cy.loginAs(activeUser);
735
736             const files = ["foobar", "anotherFile", "", "finalName"];
737
738             cy.goToPath(`/collections/${testCollection.uuid}`);
739             cy.get("[data-cy=collection-files-panel]").should("contain", "This collection is empty");
740             cy.get("[data-cy=collection-files-panel]").should("not.contain", files[0]);
741             cy.get("[data-cy=collection-info-panel]").should("contain", collName);
742
743             files.map((fileName, i, files) => {
744                 cy.updateCollection(adminUser.token, testCollection.uuid, {
745                     name: `${collName + " updated"}`,
746                     manifest_text: fileName ? `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n` : "",
747                 }).as("updatedCollection");
748                 cy.getAll("@updatedCollection").then(function ([updatedCollection]) {
749                     expect(updatedCollection.name).to.equal(`${collName + " updated"}`);
750                     cy.get("[data-cy=collection-info-panel]").should("contain", updatedCollection.name);
751                     fileName
752                         ? cy.get("[data-cy=collection-files-panel]").should("contain", fileName)
753                         : cy.get("[data-cy=collection-files-panel]").should("not.contain", files[i - 1]);
754                 });
755             });
756         });
757     });
758
759     it("makes a copy of an existing collection", function () {
760         const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
761         const copyName = `Copy of: ${collName}`;
762
763         cy.createCollection(adminUser.token, {
764             name: collName,
765             owner_uuid: activeUser.user.uuid,
766             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
767         })
768             .as("collection")
769             .then(function () {
770                 cy.loginAs(activeUser);
771                 cy.goToPath(`/collections/${this.collection.uuid}`);
772                 cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
773                 cy.get("[data-cy=collection-panel-options-btn]").click();
774                 cy.get("[data-cy=context-menu]").contains("Make a copy").click();
775                 cy.get("[data-cy=form-dialog]")
776                     .should("contain", "Make a copy")
777                     .within(() => {
778                         cy.get("[data-cy=projects-tree-home-tree-picker]").contains("Projects").click();
779                         cy.get("[data-cy=form-submit-btn]").click();
780                     });
781                 cy.get("[data-cy=snackbar]").contains("Collection has been copied.");
782                 cy.get("[data-cy=snackbar-goto-action]").click();
783                 cy.get("[data-cy=project-panel]").contains(copyName).click();
784                 cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
785             });
786     });
787
788     it("uses the collection version browser to view a previous version", function () {
789         const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
790
791         // Creates the collection using the admin token so we can set up
792         // a bogus manifest text without block signatures.
793         cy.createCollection(adminUser.token, {
794             name: colName,
795             owner_uuid: activeUser.user.uuid,
796             preserve_version: true,
797             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
798         })
799             .as("collection")
800             .then(function () {
801                 // Visit collection, check basic information
802                 cy.loginAs(activeUser);
803                 cy.goToPath(`/collections/${this.collection.uuid}`);
804
805                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
806                 cy.get("[data-cy=read-only-icon]").should("not.exist");
807                 cy.get("[data-cy=collection-version-number]").should("contain", "1");
808                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
809                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
810
811                 // Modify collection, expect version number change
812                 cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
813                 cy.get("[data-cy=context-menu]").contains("Remove").click();
814                 cy.get("[data-cy=confirmation-dialog]").should("contain", "Removing file");
815                 cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
816                 cy.get("[data-cy=collection-version-number]").should("contain", "2");
817                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
818
819                 // Click on version number, check version browser. Click on past version.
820                 cy.get("[data-cy=collection-version-browser]").should("not.exist");
821                 cy.get("[data-cy=collection-version-number]").contains("2").click();
822                 cy.get("[data-cy=collection-version-browser]")
823                     .should("contain", "Nr")
824                     .and("contain", "Size")
825                     .and("contain", "Date")
826                     .within(() => {
827                         // Version 1: 6 bytes in size
828                         cy.get("[data-cy=collection-version-browser-select-1]")
829                             .should("contain", "1")
830                             .and("contain", "6 B")
831                             .and("contain", adminUser.user.full_name);
832                         // Version 2: 3 bytes in size (one file removed)
833                         cy.get("[data-cy=collection-version-browser-select-2]")
834                             .should("contain", "2")
835                             .and("contain", "3 B")
836                             .and("contain", activeUser.user.full_name);
837                         cy.get("[data-cy=collection-version-browser-select-3]").should("not.exist");
838                         cy.get("[data-cy=collection-version-browser-select-1]").click();
839                     });
840                 cy.get("[data-cy=collection-info-panel]").should("contain", "This is an old version");
841                 cy.get("[data-cy=read-only-icon]").should("exist");
842                 cy.get("[data-cy=collection-version-number]").should("contain", "1");
843                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
844                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
845
846                 // Check that only old collection action are available on context menu
847                 cy.get("[data-cy=collection-panel-options-btn]").click();
848                 cy.get("[data-cy=context-menu]").should("contain", "Restore version").and("not.contain", "Add to favorites");
849                 cy.get("body").click(); // Collapse the menu avoiding details panel expansion
850
851                 // Click on "head version" link, confirm that it's the latest version.
852                 cy.get("[data-cy=collection-info-panel]").contains("head version").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", "2");
856                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
857                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
858
859                 // Check that old collection action isn't available on context menu
860                 cy.get("[data-cy=collection-panel-options-btn]").click();
861                 cy.get("[data-cy=context-menu]").should("not.contain", "Restore version");
862                 cy.get("body").click(); // Collapse the menu avoiding details panel expansion
863
864                 // Make another change, confirm new version.
865                 cy.get("[data-cy=collection-panel-options-btn]").click();
866                 cy.get("[data-cy=context-menu]").contains("Edit collection").click();
867                 cy.get("[data-cy=form-dialog]")
868                     .should("contain", "Edit Collection")
869                     .within(() => {
870                         // appends some text
871                         cy.get("input").first().type(" renamed");
872                     });
873                 cy.get("[data-cy=form-submit-btn]").click();
874                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
875                 cy.get("[data-cy=read-only-icon]").should("not.exist");
876                 cy.get("[data-cy=collection-version-number]").should("contain", "3");
877                 cy.get("[data-cy=collection-info-panel]").should("contain", colName + " renamed");
878                 cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
879                 cy.get("[data-cy=collection-version-browser-select-3]").should("contain", "3").and("contain", "3 B");
880
881                 // Check context menus on version browser
882                 cy.waitForDom();
883                 cy.get("[data-cy=collection-version-browser-select-3]").rightclick();
884                 cy.get("[data-cy=context-menu]")
885                     .should("contain", "Add to favorites")
886                     .and("contain", "Make a copy")
887                     .and("contain", "Edit collection");
888                 cy.get("body").click();
889                 // (and now an old version...)
890                 cy.get("[data-cy=collection-version-browser-select-1]").rightclick();
891                 cy.get("[data-cy=context-menu]")
892                     .should("not.contain", "Add to favorites")
893                     .and("contain", "Make a copy")
894                     .and("not.contain", "Edit collection");
895                 cy.get("body").click();
896
897                 // Restore first version
898                 cy.get("[data-cy=collection-version-browser]").within(() => {
899                     cy.get("[data-cy=collection-version-browser-select-1]").click();
900                 });
901                 cy.get("[data-cy=collection-panel-options-btn]").click();
902                 cy.get("[data-cy=context-menu]").contains("Restore version").click();
903                 cy.get("[data-cy=confirmation-dialog]").should("contain", "Restore version");
904                 cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
905                 cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
906                 cy.get("[data-cy=collection-version-number]").should("contain", "4");
907                 cy.get("[data-cy=collection-info-panel]").should("contain", colName);
908                 cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
909             });
910     });
911
912     it("copies selected files into new collection", () => {
913         cy.createCollection(adminUser.token, {
914             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
915             owner_uuid: activeUser.user.uuid,
916             preserve_version: true,
917             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
918         })
919             .as("collection")
920             .then(function () {
921                 // Visit collection, check basic information
922                 cy.loginAs(activeUser);
923                 cy.goToPath(`/collections/${this.collection.uuid}`);
924
925                 cy.get("[data-cy=collection-files-panel]").within(() => {
926                     cy.get("input[type=checkbox]").first().click();
927                 });
928
929                 cy.get("[data-cy=collection-files-panel-options-btn]").click();
930                 cy.get("[data-cy=context-menu]").contains("Copy selected into new collection").click();
931
932                 cy.get("[data-cy=form-dialog]").contains("Projects").click();
933
934                 cy.get("[data-cy=form-submit-btn]").click();
935
936                 cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
937
938                 cy.waitForDom().get("main").contains(`Files extracted from: ${this.collection.name}`).click();
939                 cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
940             });
941     });
942
943     it("copies selected files into existing collection", () => {
944         cy.createCollection(adminUser.token, {
945             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
946             owner_uuid: activeUser.user.uuid,
947             preserve_version: true,
948             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
949         }).as("sourceCollection");
950
951         cy.createCollection(adminUser.token, {
952             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
953             owner_uuid: activeUser.user.uuid,
954             preserve_version: true,
955             manifest_text: "",
956         }).as("destinationCollection");
957
958         cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
959             // Visit collection, check basic information
960             cy.loginAs(activeUser);
961             cy.goToPath(`/collections/${sourceCollection.uuid}`);
962
963             cy.get("[data-cy=collection-files-panel]").within(() => {
964                 cy.get("input[type=checkbox]").first().click();
965             });
966
967             cy.get("[data-cy=collection-files-panel-options-btn]").click();
968             cy.get("[data-cy=context-menu]").contains("Copy selected into existing collection").click();
969
970             cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
971
972             cy.get("[data-cy=form-submit-btn]").click();
973             cy.wait(2000);
974
975             cy.goToPath(`/collections/${destinationCollection.uuid}`);
976
977             cy.get("main").contains(destinationCollection.name).should("exist");
978             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
979         });
980     });
981
982     it("copies selected files into separate collections", () => {
983         cy.createCollection(adminUser.token, {
984             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
985             owner_uuid: activeUser.user.uuid,
986             preserve_version: true,
987             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
988         }).as("sourceCollection");
989
990         cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
991             // Visit collection, check basic information
992             cy.loginAs(activeUser);
993             cy.goToPath(`/collections/${sourceCollection.uuid}`);
994
995             // Select both files
996             cy.waitForDom()
997                 .get("[data-cy=collection-files-panel]")
998                 .within(() => {
999                     cy.get("input[type=checkbox]").first().click();
1000                     cy.get("input[type=checkbox]").last().click();
1001                 });
1002
1003             // Copy to separate collections
1004             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1005             cy.get("[data-cy=context-menu]").contains("Copy selected into separate collections").click();
1006             cy.get("[data-cy=form-dialog]").contains("Projects").click();
1007             cy.get("[data-cy=form-submit-btn]").click();
1008
1009             // Verify created collections
1010             cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
1011             cy.get("main").contains(`File copied from collection ${sourceCollection.name}/foo`).click();
1012             cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
1013             cy.get(".layout-pane-primary").contains("Projects").click();
1014             cy.get("main").contains(`File copied from collection ${sourceCollection.name}/bar`).click();
1015             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1016
1017             // Verify separate collection menu items not present when single file selected
1018             // Wait for dom for collection to re-render
1019             cy.waitForDom()
1020                 .get("[data-cy=collection-files-panel]")
1021                 .within(() => {
1022                     cy.get("input[type=checkbox]").first().click();
1023                 });
1024             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1025             cy.get("[data-cy=context-menu]").should("not.contain", "Copy selected into separate collections");
1026             cy.get("[data-cy=context-menu]").should("not.contain", "Move selected into separate collections");
1027         });
1028     });
1029
1030     it("moves selected files into new collection", () => {
1031         cy.createCollection(adminUser.token, {
1032             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1033             owner_uuid: activeUser.user.uuid,
1034             preserve_version: true,
1035             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1036         })
1037             .as("collection")
1038             .then(function () {
1039                 // Visit collection, check basic information
1040                 cy.loginAs(activeUser);
1041                 cy.goToPath(`/collections/${this.collection.uuid}`);
1042
1043                 cy.get("[data-cy=collection-files-panel]").within(() => {
1044                     cy.get("input[type=checkbox]").first().click();
1045                 });
1046
1047                 cy.get("[data-cy=collection-files-panel-options-btn]").click();
1048                 cy.get("[data-cy=context-menu]").contains("Move selected into new collection").click();
1049
1050                 cy.get("[data-cy=form-dialog]").contains("Projects").click();
1051
1052                 cy.get("[data-cy=form-submit-btn]").click();
1053
1054                 cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
1055
1056                 cy.get("main").contains(`Files moved from: ${this.collection.name}`).click();
1057                 cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1058             });
1059     });
1060
1061     it("moves selected files into existing collection", () => {
1062         cy.createCollection(adminUser.token, {
1063             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1064             owner_uuid: activeUser.user.uuid,
1065             preserve_version: true,
1066             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1067         }).as("sourceCollection");
1068
1069         cy.createCollection(adminUser.token, {
1070             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
1071             owner_uuid: activeUser.user.uuid,
1072             preserve_version: true,
1073             manifest_text: "",
1074         }).as("destinationCollection");
1075
1076         cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
1077             // Visit collection, check basic information
1078             cy.loginAs(activeUser);
1079             cy.goToPath(`/collections/${sourceCollection.uuid}`);
1080
1081             cy.get("[data-cy=collection-files-panel]").within(() => {
1082                 cy.get("input[type=checkbox]").first().click();
1083             });
1084
1085             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1086             cy.get("[data-cy=context-menu]").contains("Move selected into existing collection").click();
1087
1088             cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
1089
1090             cy.get("[data-cy=form-submit-btn]").click();
1091             cy.wait(2000);
1092
1093             cy.goToPath(`/collections/${destinationCollection.uuid}`);
1094
1095             cy.get("main").contains(destinationCollection.name).should("exist");
1096             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1097         });
1098     });
1099
1100     it("moves selected files into separate collections", () => {
1101         cy.createCollection(adminUser.token, {
1102             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
1103             owner_uuid: activeUser.user.uuid,
1104             preserve_version: true,
1105             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
1106         }).as("sourceCollection");
1107
1108         cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
1109             // Visit collection, check basic information
1110             cy.loginAs(activeUser);
1111             cy.goToPath(`/collections/${sourceCollection.uuid}`);
1112
1113             // Select both files
1114             cy.get("[data-cy=collection-files-panel]").within(() => {
1115                 cy.get("input[type=checkbox]").first().click();
1116                 cy.get("input[type=checkbox]").last().click();
1117             });
1118
1119             // Copy to separate collections
1120             cy.get("[data-cy=collection-files-panel-options-btn]").click();
1121             cy.get("[data-cy=context-menu]").contains("Move selected into separate collections").click();
1122             cy.get("[data-cy=form-dialog]").contains("Projects").click();
1123             cy.get("[data-cy=form-submit-btn]").click();
1124
1125             // Verify created collections
1126             cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
1127             cy.get("main").contains(`File moved from collection ${sourceCollection.name}/foo`).click();
1128             cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
1129             cy.get(".layout-pane-primary").contains("Projects").click();
1130             cy.get("main").contains(`File moved from collection ${sourceCollection.name}/bar`).click();
1131             cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
1132         });
1133     });
1134
1135     it("creates new collection with properties on home project", function () {
1136         cy.loginAs(activeUser);
1137         cy.goToPath(`/projects/${activeUser.user.uuid}`);
1138         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
1139         cy.get("[data-cy=breadcrumb-last]").should("not.exist");
1140         // Create new collection
1141         cy.get("[data-cy=side-panel-button]").click();
1142         cy.get("[data-cy=side-panel-new-collection]").click();
1143         // Name between brackets tests bugfix #17582
1144         const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
1145
1146         // Select a storage class.
1147         cy.get("[data-cy=form-dialog]")
1148             .should("contain", "New collection")
1149             .and("contain", "Storage classes")
1150             .and("contain", "default")
1151             .and("contain", "foo")
1152             .and("contain", "bar")
1153             .within(() => {
1154                 cy.get("[data-cy=parent-field]").within(() => {
1155                     cy.get("input").should("have.value", "Home project");
1156                 });
1157                 cy.get("[data-cy=name-field]").within(() => {
1158                     cy.get("input").type(collName);
1159                 });
1160                 cy.get("[data-cy=checkbox-foo]").click();
1161             });
1162
1163         // Add a property.
1164         // Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
1165         cy.get("[data-cy=form-dialog]").should("not.contain", "Color: Magenta");
1166         cy.get("[data-cy=resource-properties-form]").within(() => {
1167             cy.get("[data-cy=property-field-key]").within(() => {
1168                 cy.get("input").type("Color");
1169             });
1170             cy.get("[data-cy=property-field-value]").within(() => {
1171                 cy.get("input").type("Magenta");
1172             });
1173             cy.root().submit();
1174         });
1175         // Confirm proper vocabulary labels are displayed on the UI.
1176         cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
1177
1178         // Value field should not complain about being required just after
1179         // adding a new property. See #19732
1180         cy.get("[data-cy=form-dialog]").should("not.contain", "This field is required");
1181
1182         cy.get("[data-cy=form-submit-btn]").click();
1183         // Confirm that the user was taken to the newly created collection
1184         cy.get("[data-cy=form-dialog]").should("not.exist");
1185         cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
1186         cy.get("[data-cy=breadcrumb-last]").should("contain", collName);
1187         cy.get("[data-cy=collection-info-panel]")
1188             .should("contain", "default")
1189             .and("contain", "foo")
1190             .and("contain", "Color: Magenta")
1191             .and("not.contain", "bar");
1192         // Confirm that the collection's properties has the real values.
1193         cy.doRequest("GET", "/arvados/v1/collections", null, {
1194             filters: `[["name", "=", "${collName}"]]`,
1195         })
1196             .its("body.items")
1197             .as("collections")
1198             .then(function () {
1199                 expect(this.collections).to.have.lengthOf(1);
1200                 expect(this.collections[0].properties).to.have.property("IDTAGCOLORS", "IDVALCOLORS3");
1201             });
1202     });
1203
1204     it("shows responsible person for collection if available", () => {
1205         cy.createCollection(adminUser.token, {
1206             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1207             owner_uuid: activeUser.user.uuid,
1208             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1209         }).as("testCollection1");
1210
1211         cy.createCollection(adminUser.token, {
1212             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1213             owner_uuid: adminUser.user.uuid,
1214             manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1215         })
1216             .as("testCollection2")
1217             .then(function (testCollection2) {
1218                 cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, "can_write");
1219             });
1220
1221         cy.getAll("@testCollection1", "@testCollection2").then(function ([testCollection1, testCollection2]) {
1222             cy.loginAs(activeUser);
1223
1224             cy.goToPath(`/collections/${testCollection1.uuid}`);
1225             cy.get("[data-cy=responsible-person-wrapper]").contains(activeUser.user.uuid);
1226
1227             cy.goToPath(`/collections/${testCollection2.uuid}`);
1228             cy.get("[data-cy=responsible-person-wrapper]").contains(adminUser.user.uuid);
1229         });
1230     });
1231
1232     describe("file upload", () => {
1233         beforeEach(() => {
1234             cy.createCollection(adminUser.token, {
1235                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1236                 owner_uuid: activeUser.user.uuid,
1237                 manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
1238             }).as("testCollection1");
1239         });
1240
1241         it("uploads a file and checks the collection UI to be fresh", () => {
1242             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1243                 cy.loginAs(activeUser);
1244                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1245                 cy.get("[data-cy=upload-button]").click();
1246                 cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
1247                 cy.get("[data-cy=collection-file-count]").should("contain", "2");
1248                 cy.fixture("files/5mb.bin", "base64").then(content => {
1249                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1250                     cy.get("[data-cy=form-submit-btn]").click();
1251                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1252                     cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
1253                     cy.get("[data-cy=collection-file-count]").should("contain", "3");
1254
1255                     cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
1256                     cy.get("[data-cy=upload-button]").click();
1257                     cy.fixture("files/5mb.bin", "base64").then(content => {
1258                         cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1259                         cy.get("[data-cy=form-submit-btn]").click();
1260                         cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1261                         // subdir gets unselected, I think this is a bug but
1262                         // for the time being let's just make sure the test works.
1263                         cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
1264                         cy.waitForDom().get("[data-cy=collection-files-right-panel]").contains("5mb_b.bin").should("exist");
1265                     });
1266                 });
1267             });
1268         });
1269
1270         it("allows to cancel running upload", () => {
1271             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1272                 cy.loginAs(activeUser);
1273
1274                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1275
1276                 cy.get("[data-cy=upload-button]").click();
1277
1278                 cy.fixture("files/5mb.bin", "base64").then(content => {
1279                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1280                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1281
1282                     cy.get("[data-cy=form-submit-btn]").click();
1283
1284                     cy.get("button").contains("Cancel").click();
1285
1286                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1287                 });
1288             });
1289         });
1290
1291         it("allows to cancel single file from the running upload", () => {
1292             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1293                 cy.loginAs(activeUser);
1294
1295                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1296
1297                 cy.get("[data-cy=upload-button]").click();
1298
1299                 cy.fixture("files/5mb.bin", "base64").then(content => {
1300                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1301                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1302
1303                     cy.get("[data-cy=form-submit-btn]").click();
1304
1305                     cy.get("button[aria-label=Remove]").eq(1).click();
1306
1307                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1308
1309                     cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
1310                 });
1311             });
1312         });
1313
1314         it("allows to cancel all files from the running upload", () => {
1315             cy.getAll("@testCollection1").then(function ([testCollection1]) {
1316                 cy.loginAs(activeUser);
1317
1318                 cy.goToPath(`/collections/${testCollection1.uuid}`);
1319
1320                 // Confirm initial collection state.
1321                 cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
1322                 cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
1323                 cy.get("[data-cy=collection-files-panel]").contains("5mb_b.bin").should("not.exist");
1324
1325                 cy.get("[data-cy=upload-button]").click();
1326
1327                 cy.fixture("files/5mb.bin", "base64").then(content => {
1328                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
1329                     cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
1330
1331                     cy.get("[data-cy=form-submit-btn]").click();
1332
1333                     cy.get("button[aria-label=Remove]").should("exist");
1334                     cy.get("button[aria-label=Remove]").click({ multiple: true, force: true });
1335
1336                     cy.get("[data-cy=form-submit-btn]").should("not.exist");
1337
1338                     // Confirm final collection state.
1339                     cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
1340                     // The following fails, but doesn't seem to happen
1341                     // in the real world. Maybe there's a race between
1342                     // the PUT request finishing and the 'Remove' button
1343                     // dissapearing, because sometimes just one of the 2
1344                     // files gets uploaded.
1345                     // Maybe this will be needed to simulate a slow network:
1346                     // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
1347                     // cy.get('[data-cy=collection-files-panel]')
1348                     //     .contains('5mb_a.bin').should('not.exist');
1349                     // cy.get('[data-cy=collection-files-panel]')
1350                     //     .contains('5mb_b.bin').should('not.exist');
1351                 });
1352             });
1353         });
1354     });
1355 });