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