21700: Install Bundler system-wide in Rails postinst
[arvados.git] / services / workbench2 / cypress / e2e / process.cy.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { ContainerState } from "models/container";
6
7 describe("Process tests", function () {
8     let activeUser;
9     let adminUser;
10
11     before(function () {
12         // Only set up common users once. These aren't set up as aliases because
13         // aliases are cleaned up after every test. Also it doesn't make sense
14         // to set the same users on beforeEach() over and over again, so we
15         // separate a little from Cypress' 'Best Practices' here.
16         cy.getUser("admin", "Admin", "User", true, true)
17             .as("adminUser")
18             .then(function () {
19                 adminUser = this.adminUser;
20             });
21         cy.getUser("user", "Active", "User", false, true)
22             .as("activeUser")
23             .then(function () {
24                 activeUser = this.activeUser;
25             });
26     });
27
28     function setupDockerImage(image_name) {
29         // Create a collection that will be used as a docker image for the tests.
30         cy.createCollection(adminUser.token, {
31             name: "docker_image",
32             manifest_text:
33                 ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
34         })
35             .as("dockerImage")
36             .then(function (dockerImage) {
37                 // Give read permissions to the active user on the docker image.
38                 cy.createLink(adminUser.token, {
39                     link_class: "permission",
40                     name: "can_read",
41                     tail_uuid: activeUser.user.uuid,
42                     head_uuid: dockerImage.uuid,
43                 })
44                     .as("dockerImagePermission")
45                     .then(function () {
46                         // Set-up docker image collection tags
47                         cy.createLink(activeUser.token, {
48                             link_class: "docker_image_repo+tag",
49                             name: image_name,
50                             head_uuid: dockerImage.uuid,
51                         }).as("dockerImageRepoTag");
52                         cy.createLink(activeUser.token, {
53                             link_class: "docker_image_hash",
54                             name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
55                             head_uuid: dockerImage.uuid,
56                         }).as("dockerImageHash");
57                     });
58             });
59         return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {
60             return dockerImage;
61         });
62     }
63
64     function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
65         return setupDockerImage(docker_image).then(function (dockerImage) {
66             return cy.createContainerRequest(user.token, {
67                 name: name,
68                 command: command,
69                 container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
70                 output_path: "stdout.txt",
71                 priority: 1,
72                 runtime_constraints: {
73                     vcpus: 1,
74                     ram: 1,
75                 },
76                 use_existing: reuse,
77                 state: state,
78                 mounts: {
79                     foo: {
80                         kind: "tmp",
81                         path: "/tmp/foo",
82                     },
83                 },
84             });
85         });
86     }
87
88     describe('Multiselect Toolbar', () => {
89         it('shows the appropriate buttons in the toolbar', () => {
90
91             const msButtonTooltips = [
92                 'View details',
93                 'Open in new tab',
94                 'Outputs',
95                 'API Details',
96                 'Edit process',
97                 'Copy and re-run process',
98                 'CANCEL',
99                 'Remove',
100                 'Add to favorites',
101             ];
102
103             createContainerRequest(
104                 activeUser,
105                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
106                 "arvados/jobs",
107                 ["echo", "hello world"],
108                 false,
109                 "Committed"
110             ).then(function (containerRequest) {
111                 cy.loginAs(activeUser);
112                 cy.goToPath(`/processes/${containerRequest.uuid}`);
113                 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
114                 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
115                 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
116                 cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
117                 cy.waitForDom()
118                 cy.get('[data-cy=data-table-row]').contains(containerRequest.name).should('exist').parent().parent().parent().parent().click()
119                 cy.waitForDom()
120                 cy.get('[data-cy=multiselect-button]').should('have.length', msButtonTooltips.length)
121                 for (let i = 0; i < msButtonTooltips.length; i++) {
122                     cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseover');
123                     cy.get('body').contains(msButtonTooltips[i]).should('exist')
124                     cy.get('[data-cy=multiselect-button]').eq(i).trigger('mouseout');
125                 }
126             });
127         })
128     })
129
130     describe("Details panel", function () {
131         it("shows process details", function () {
132             createContainerRequest(
133                 activeUser,
134                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
135                 "arvados/jobs",
136                 ["echo", "hello world"],
137                 false,
138                 "Committed"
139             ).then(function (containerRequest) {
140                 cy.loginAs(activeUser);
141                 cy.goToPath(`/processes/${containerRequest.uuid}`);
142                 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
143                 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
144                 cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
145             });
146
147             // Fake submitted by another user
148             cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
149                 req.on('response', res => {
150                     res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
151                 });
152             });
153
154             createContainerRequest(
155                 activeUser,
156                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
157                 "arvados/jobs",
158                 ["echo", "hello world"],
159                 false,
160                 "Committed"
161             ).then(function (containerRequest) {
162                 cy.loginAs(activeUser);
163                 cy.goToPath(`/processes/${containerRequest.uuid}`);
164                 cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
165                 cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
166                 cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
167             });
168         });
169
170         it("should show runtime status indicators", function () {
171             // Setup running container with runtime_status error & warning messages
172             createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
173                 .as("containerRequest")
174                 .then(function (containerRequest) {
175                     expect(containerRequest.state).to.equal("Committed");
176                     expect(containerRequest.container_uuid).not.to.be.equal("");
177
178                     cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
179                         expect(queuedContainer.state).to.be.equal("Queued");
180                     });
181                     cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
182                         state: "Locked",
183                     }).then(function (lockedContainer) {
184                         expect(lockedContainer.state).to.be.equal("Locked");
185
186                         cy.updateContainer(adminUser.token, lockedContainer.uuid, {
187                             state: "Running",
188                             runtime_status: {
189                                 error: "Something went wrong",
190                                 errorDetail: "Process exited with status 1",
191                                 warning: "Free disk space is low",
192                             },
193                         })
194                             .as("runningContainer")
195                             .then(function (runningContainer) {
196                                 expect(runningContainer.state).to.be.equal("Running");
197                                 expect(runningContainer.runtime_status).to.be.deep.equal({
198                                     error: "Something went wrong",
199                                     errorDetail: "Process exited with status 1",
200                                     warning: "Free disk space is low",
201                                 });
202                             });
203                     });
204                 });
205             // Test that the UI shows the error and warning messages
206             cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
207                 cy.loginAs(activeUser);
208                 cy.goToPath(`/processes/${containerRequest.uuid}`);
209                 cy.get("[data-cy=process-runtime-status-error]")
210                     .should("contain", "Something went wrong")
211                     .and("contain", "Process exited with status 1");
212                 cy.get("[data-cy=process-runtime-status-warning]")
213                     .should("contain", "Free disk space is low")
214                     .and("contain", "No additional warning details available");
215             });
216
217             // Force container_count for testing
218             let containerCount = 2;
219             cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
220                 req.on('response', res => {
221                     res.body.container_count = containerCount;
222                 });
223             });
224
225             cy.getAll("@containerRequest").then(function ([containerRequest]) {
226                 cy.goToPath(`/processes/${containerRequest.uuid}`);
227                 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
228             });
229
230             cy.getAll("@containerRequest").then(function ([containerRequest]) {
231                 containerCount = 3;
232                 cy.goToPath(`/processes/${containerRequest.uuid}`);
233                 cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
234             });
235         });
236
237         it("allows copying processes", function () {
238             const crName = "first_container_request";
239             const copiedCrName = "copied_container_request";
240             createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
241                 cy.loginAs(activeUser);
242                 cy.goToPath(`/processes/${containerRequest.uuid}`);
243                 cy.get("[data-cy=process-details]").should("contain", crName);
244
245                 cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
246                 cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
247             });
248
249             cy.get("[data-cy=form-dialog]").within(() => {
250                 cy.get("input[name=name]").clear().type(copiedCrName);
251                 cy.get("[data-cy=projects-tree-home-tree-picker]").click();
252                 cy.get("[data-cy=form-submit-btn]").click();
253             });
254
255             cy.get("[data-cy=process-details]").should("contain", copiedCrName);
256             cy.get("[data-cy=process-details]").find("button").contains("Run");
257         });
258
259         const getFakeContainer = fakeContainerUuid => ({
260             href: `/containers/${fakeContainerUuid}`,
261             kind: "arvados#container",
262             etag: "ecfosljpnxfari9a8m7e4yv06",
263             uuid: fakeContainerUuid,
264             owner_uuid: "zzzzz-tpzed-000000000000000",
265             created_at: "2023-02-13T15:55:47.308915000Z",
266             modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
267             modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
268             modified_at: "2023-02-15T19:12:45.987086000Z",
269             command: [
270                 "arvados-cwl-runner",
271                 "--api=containers",
272                 "--local",
273                 "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
274                 "/var/lib/cwl/workflow.json#main",
275                 "/var/lib/cwl/cwl.input.json",
276             ],
277             container_image: "4ad7d11381df349e464694762db14e04+303",
278             cwd: "/var/spool/cwl",
279             environment: {},
280             exit_code: null,
281             finished_at: null,
282             locked_by_uuid: null,
283             log: null,
284             output: null,
285             output_path: "/var/spool/cwl",
286             progress: null,
287             runtime_constraints: {
288                 API: true,
289                 cuda: {
290                     device_count: 0,
291                     driver_version: "",
292                     hardware_capability: "",
293                 },
294                 keep_cache_disk: 2147483648,
295                 keep_cache_ram: 0,
296                 ram: 1342177280,
297                 vcpus: 1,
298             },
299             runtime_status: {},
300             started_at: null,
301             auth_uuid: null,
302             scheduling_parameters: {
303                 max_run_time: 0,
304                 partitions: [],
305                 preemptible: false,
306             },
307             runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
308             runtime_auth_scopes: ["all"],
309             lock_count: 2,
310             gateway_address: null,
311             interactive_session_started: false,
312             output_storage_classes: ["default"],
313             output_properties: {},
314             cost: 0.0,
315             subrequests_cost: 0.0,
316         });
317
318         it("shows cancel button when appropriate", function () {
319             // Ignore collection requests
320             cy.intercept(
321                 { method: "GET", url: `**/arvados/v1/collections/*` },
322                 {
323                     statusCode: 200,
324                     body: {},
325                 }
326             );
327
328             // Uncommitted container
329             const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
330             createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
331                 containerRequest
332             ) {
333                 cy.loginAs(activeUser);
334                 // Navigate to process and verify run / cancel button
335                 cy.goToPath(`/processes/${containerRequest.uuid}`);
336                 cy.waitForDom();
337                 cy.get("[data-cy=process-details]").should("contain", crUncommitted);
338                 cy.get("[data-cy=process-run-button]").should("exist");
339                 cy.get("[data-cy=process-cancel-button]").should("not.exist");
340             });
341
342             // Queued container
343             const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
344             const fakeCrUuid = "zzzzz-dz642-000000000000001";
345             createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
346                 containerRequest
347             ) {
348                 // Fake container uuid
349                 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
350                     req.on('response', res => {
351                         res.body.output_uuid = fakeCrUuid;
352                         res.body.priority = 500;
353                         res.body.state = "Committed";
354                     });
355                 });
356
357                 // Fake container
358                 const container = getFakeContainer(fakeCrUuid);
359                 cy.intercept(
360                     { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
361                     {
362                         statusCode: 200,
363                         body: { ...container, state: "Queued", priority: 500 },
364                     }
365                 );
366
367                 // Navigate to process and verify cancel button
368                 cy.goToPath(`/processes/${containerRequest.uuid}`);
369                 cy.waitForDom();
370                 cy.get("[data-cy=process-details]").should("contain", crQueued);
371                 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
372             });
373
374             // Locked container
375             const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
376             const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
377             createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
378                 containerRequest
379             ) {
380                 // Fake container uuid
381                 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
382                     req.on('response', res => {
383                         res.body.output_uuid = fakeCrLockedUuid;
384                         res.body.priority = 500;
385                         res.body.state = "Committed";
386                     });
387                 });
388
389                 // Fake container
390                 const container = getFakeContainer(fakeCrLockedUuid);
391                 cy.intercept(
392                     { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
393                     {
394                         statusCode: 200,
395                         body: { ...container, state: "Locked", priority: 500 },
396                     }
397                 );
398
399                 // Navigate to process and verify cancel button
400                 cy.goToPath(`/processes/${containerRequest.uuid}`);
401                 cy.waitForDom();
402                 cy.get("[data-cy=process-details]").should("contain", crLocked);
403                 cy.get("[data-cy=process-cancel-button]").contains("Cancel");
404             });
405
406             // On Hold container
407             const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
408             const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
409             createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
410                 containerRequest
411             ) {
412                 // Fake container uuid
413                 cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
414                     req.on('response', res => {
415                         res.body.output_uuid = fakeCrOnHoldUuid;
416                         res.body.priority = 0;
417                         res.body.state = "Committed";
418                     });
419                 });
420
421                 // Fake container
422                 const container = getFakeContainer(fakeCrOnHoldUuid);
423                 cy.intercept(
424                     { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
425                     {
426                         statusCode: 200,
427                         body: { ...container, state: "Queued", priority: 0 },
428                     }
429                 );
430
431                 // Navigate to process and verify cancel button
432                 cy.goToPath(`/processes/${containerRequest.uuid}`);
433                 cy.waitForDom();
434                 cy.get("[data-cy=process-details]").should("contain", crOnHold);
435                 cy.get("[data-cy=process-run-button]").should("exist");
436                 cy.get("[data-cy=process-cancel-button]").should("not.exist");
437             });
438         });
439     });
440
441     describe("Logs panel", function () {
442         it("shows live process logs", function () {
443             cy.intercept({ method: "GET", url: "**/arvados/v1/containers/*" }, req => {
444                 req.on('response', res => {
445                     res.body.state = ContainerState.RUNNING;
446                 });
447             });
448
449             const crName = "test_container_request";
450             createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
451                 // Create empty log file before loading process page
452                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
453
454                 cy.loginAs(activeUser);
455                 cy.goToPath(`/processes/${containerRequest.uuid}`);
456                 cy.get("[data-cy=process-details]").should("contain", crName);
457                 cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
458
459                 // Append a log line
460                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
461                     cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
462                 });
463
464                 // Append new log line to different file
465                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
466                     cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
467                 });
468             });
469         });
470
471         it("filters process logs by event type", function () {
472             const nodeInfoLogs = [
473                 "Host Information",
474                 "Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux",
475                 "CPU Information",
476                 "processor  : 0",
477                 "vendor_id  : GenuineIntel",
478                 "cpu family : 6",
479                 "model      : 79",
480                 "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
481             ];
482             const crunchRunLogs = [
483                 "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
484                 "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
485                 "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
486                 "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
487                 "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
488             ];
489             const stdoutLogs = [
490                 "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
491                 "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
492                 "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
493                 "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
494                 "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
495                 "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
496                 "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
497                 "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
498                 "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
499                 "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
500                 "2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.",
501                 "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
502                 "2022-03-22T13:56:22.542418117Z Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.",
503                 "2022-03-22T13:56:22.542418127Z Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.",
504                 "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
505                 "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
506                 "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
507                 "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
508             ];
509
510             createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
511                 containerRequest
512             ) {
513                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
514                 cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
515                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
516
517                 cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
518                     cy.loginAs(activeUser);
519                     cy.goToPath(`/processes/${containerRequest.uuid}`);
520                     // Should show main logs by default
521                     cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
522                     cy.get("[data-cy=process-logs]")
523                         .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
524                         .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
525                         .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
526                     // Select 'All logs'
527                     cy.get("[data-cy=process-logs-filter]").click();
528                     cy.get("body").contains("li", "All logs").click();
529                     cy.get("[data-cy=process-logs]")
530                         .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
531                         .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
532                         .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
533                     // Select 'node-info' logs
534                     cy.get("[data-cy=process-logs-filter]").click();
535                     cy.get("body").contains("li", "node-info").click();
536                     cy.get("[data-cy=process-logs]")
537                         .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
538                         .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
539                         .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
540                     // Select 'stdout' logs
541                     cy.get("[data-cy=process-logs-filter]").click();
542                     cy.get("body").contains("li", "stdout").click();
543                     cy.get("[data-cy=process-logs]")
544                         .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
545                         .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
546                         .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
547                 });
548             });
549         });
550
551         it("sorts combined logs", function () {
552             const crName = "test_container_request";
553             createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
554                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
555                     "3: nodeinfo 1",
556                     "2: nodeinfo 2",
557                     "1: nodeinfo 3",
558                     "2: nodeinfo 4",
559                     "3: nodeinfo 5",
560                 ]).as("node-info");
561
562                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
563                     "2023-07-18T20:14:48.128642814Z first",
564                     "2023-07-18T20:14:49.128642814Z third",
565                 ]).as("stdout");
566
567                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
568
569                 cy.loginAs(activeUser);
570                 cy.goToPath(`/processes/${containerRequest.uuid}`);
571                 cy.get("[data-cy=process-details]").should("contain", crName);
572                 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
573
574                 cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
575                     // Verify sorted main logs
576                     cy.get("[data-cy=process-logs] span > p", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
577                     cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
578                     cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
579
580                     // Switch to All logs
581                     cy.get("[data-cy=process-logs-filter]").click();
582                     cy.get("body").contains("li", "All logs").click();
583                     // Verify non-sorted lines were preserved
584                     cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "3: nodeinfo 1");
585                     cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2: nodeinfo 2");
586                     cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "1: nodeinfo 3");
587                     cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2: nodeinfo 4");
588                     cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "3: nodeinfo 5");
589                     // Verify sorted logs
590                     cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
591                     cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
592                     cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
593                 });
594             });
595         });
596
597         it("preserves original ordering of lines within the same log type", function () {
598             const crName = "test_container_request";
599             createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
600                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
601                     // Should come first
602                     "2023-07-18T20:14:46.000000000Z A out 1",
603                     // Comes fourth in a contiguous block
604                     "2023-07-18T20:14:48.128642814Z A out 2",
605                     "2023-07-18T20:14:48.128642814Z X out 3",
606                     "2023-07-18T20:14:48.128642814Z A out 4",
607                 ]).as("stdout");
608
609                 cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
610                     // Comes second
611                     "2023-07-18T20:14:47.000000000Z Z err 1",
612                     // Comes third in a contiguous block
613                     "2023-07-18T20:14:48.128642814Z B err 2",
614                     "2023-07-18T20:14:48.128642814Z C err 3",
615                     "2023-07-18T20:14:48.128642814Z Y err 4",
616                     "2023-07-18T20:14:48.128642814Z Z err 5",
617                     "2023-07-18T20:14:48.128642814Z A err 6",
618                 ]).as("stderr");
619
620                 cy.loginAs(activeUser);
621                 cy.goToPath(`/processes/${containerRequest.uuid}`);
622                 cy.get("[data-cy=process-details]").should("contain", crName);
623                 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
624
625                 cy.getAll("@stdout", "@stderr").then(() => {
626                     // Switch to All logs
627                     cy.get("[data-cy=process-logs-filter]").click();
628                     cy.get("body").contains("li", "All logs").click();
629                     // Verify sorted logs
630                     cy.get("[data-cy=process-logs] span > p").eq(0).should("contain", "2023-07-18T20:14:46.000000000Z A out 1");
631                     cy.get("[data-cy=process-logs] span > p").eq(1).should("contain", "2023-07-18T20:14:47.000000000Z Z err 1");
632                     cy.get("[data-cy=process-logs] span > p").eq(2).should("contain", "2023-07-18T20:14:48.128642814Z B err 2");
633                     cy.get("[data-cy=process-logs] span > p").eq(3).should("contain", "2023-07-18T20:14:48.128642814Z C err 3");
634                     cy.get("[data-cy=process-logs] span > p").eq(4).should("contain", "2023-07-18T20:14:48.128642814Z Y err 4");
635                     cy.get("[data-cy=process-logs] span > p").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z Z err 5");
636                     cy.get("[data-cy=process-logs] span > p").eq(6).should("contain", "2023-07-18T20:14:48.128642814Z A err 6");
637                     cy.get("[data-cy=process-logs] span > p").eq(7).should("contain", "2023-07-18T20:14:48.128642814Z A out 2");
638                     cy.get("[data-cy=process-logs] span > p").eq(8).should("contain", "2023-07-18T20:14:48.128642814Z X out 3");
639                     cy.get("[data-cy=process-logs] span > p").eq(9).should("contain", "2023-07-18T20:14:48.128642814Z A out 4");
640                 });
641             });
642         });
643
644         it("correctly generates sniplines", function () {
645             const SNIPLINE = `================ âœ€ ================ âœ€ ========= Some log(s) were skipped ========= âœ€ ================ âœ€ ================`;
646             const crName = "test_container_request";
647             createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
648                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
649                     "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
650                 ]).as("stdout");
651
652                 cy.loginAs(activeUser);
653                 cy.goToPath(`/processes/${containerRequest.uuid}`);
654                 cy.get("[data-cy=process-details]").should("contain", crName);
655                 cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
656
657                 // Switch to stdout since lines are unsortable (no timestamp)
658                 cy.get("[data-cy=process-logs-filter]").click();
659                 cy.get("body").contains("li", "stdout").click();
660
661                 cy.getAll("@stdout").then(() => {
662                     // Verify first 64KB and snipline
663                     cy.get("[data-cy=process-logs] span > p", { timeout: 7000 })
664                         .eq(0)
665                         .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
666                     // Verify last 64KB
667                     cy.get("[data-cy=process-logs] span > p")
668                         .eq(1)
669                         .should("contain", "_" + "X".repeat(63999));
670                     // Verify none of the Os got through
671                     cy.get("[data-cy=process-logs] span > p").should("not.contain", "O");
672                 });
673             });
674         });
675
676         it("correctly break long lines when no obvious line separation exists", function () {
677             function randomString(length) {
678                 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
679                 let res = '';
680                 for (let i = 0; i < length; i++) {
681                     res += chars.charAt(Math.floor(Math.random() * chars.length));
682                 }
683                 return res;
684             }
685
686             const logLinesQty = 10;
687             const logLines = [];
688             for (let i = 0; i < logLinesQty; i++) {
689                 const length = Math.floor(Math.random() * 500) + 500;
690                 logLines.push(randomString(length));
691             }
692
693             createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
694                 containerRequest
695             ) {
696                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", logLines).as("stdoutLogs");
697
698                 cy.getAll("@stdoutLogs").then(function () {
699                     cy.loginAs(activeUser);
700                     cy.goToPath(`/processes/${containerRequest.uuid}`);
701                     // Select 'stdout' log filter
702                     cy.get("[data-cy=process-logs-filter]").click();
703                     cy.get("body").contains("li", "stdout").click();
704                     cy.get("[data-cy=process-logs] span > p")
705                         .should('have.length', logLinesQty)
706                         .each($p => {
707                             expect($p.text().length).to.be.greaterThan(499);
708
709                             // This looks like an ugly hack, but I was not able
710                             // to get [client|scroll]Width attributes through
711                             // the usual Cypress methods.
712                             const parentClientWidth = $p[0].parentElement.clientWidth;
713                             const parentScrollWidth = $p[0].parentElement.scrollWidth
714                             // Scrollbar should not be visible
715                             expect(parentClientWidth).to.be.eq(parentScrollWidth);
716                         });
717                 });
718             });
719         });
720     });
721
722     describe("I/O panel", function () {
723         const testInputs = [
724             {
725                 definition: {
726                     id: "#main/input_file",
727                     label: "Label Description",
728                     type: "File",
729                 },
730                 input: {
731                     input_file: {
732                         basename: "input1.tar",
733                         class: "File",
734                         location: "keep:00000000000000000000000000000000+01/input1.tar",
735                         secondaryFiles: [
736                             {
737                                 basename: "input1-2.txt",
738                                 class: "File",
739                                 location: "keep:00000000000000000000000000000000+01/input1-2.txt",
740                             },
741                             {
742                                 basename: "input1-3.txt",
743                                 class: "File",
744                                 location: "keep:00000000000000000000000000000000+01/input1-3.txt",
745                             },
746                             {
747                                 basename: "input1-4.txt",
748                                 class: "File",
749                                 location: "keep:00000000000000000000000000000000+01/input1-4.txt",
750                             },
751                         ],
752                     },
753                 },
754             },
755             {
756                 definition: {
757                     id: "#main/input_dir",
758                     doc: "Doc Description",
759                     type: "Directory",
760                 },
761                 input: {
762                     input_dir: {
763                         basename: "11111111111111111111111111111111+01",
764                         class: "Directory",
765                         location: "keep:11111111111111111111111111111111+01",
766                     },
767                 },
768             },
769             {
770                 definition: {
771                     id: "#main/input_bool",
772                     doc: ["Doc desc 1", "Doc desc 2"],
773                     type: "boolean",
774                 },
775                 input: {
776                     input_bool: true,
777                 },
778             },
779             {
780                 definition: {
781                     id: "#main/input_int",
782                     type: "int",
783                 },
784                 input: {
785                     input_int: 1,
786                 },
787             },
788             {
789                 definition: {
790                     id: "#main/input_long",
791                     type: "long",
792                 },
793                 input: {
794                     input_long: 1,
795                 },
796             },
797             {
798                 definition: {
799                     id: "#main/input_float",
800                     type: "float",
801                 },
802                 input: {
803                     input_float: 1.5,
804                 },
805             },
806             {
807                 definition: {
808                     id: "#main/input_double",
809                     type: "double",
810                 },
811                 input: {
812                     input_double: 1.3,
813                 },
814             },
815             {
816                 definition: {
817                     id: "#main/input_string",
818                     type: "string",
819                 },
820                 input: {
821                     input_string: "Hello World",
822                 },
823             },
824             {
825                 definition: {
826                     id: "#main/input_file_array",
827                     type: {
828                         items: "File",
829                         type: "array",
830                     },
831                 },
832                 input: {
833                     input_file_array: [
834                         {
835                             basename: "input2.tar",
836                             class: "File",
837                             location: "keep:00000000000000000000000000000000+02/input2.tar",
838                         },
839                         {
840                             basename: "input3.tar",
841                             class: "File",
842                             location: "keep:00000000000000000000000000000000+03/input3.tar",
843                             secondaryFiles: [
844                                 {
845                                     basename: "input3-2.txt",
846                                     class: "File",
847                                     location: "keep:00000000000000000000000000000000+03/input3-2.txt",
848                                 },
849                             ],
850                         },
851                         {
852                             $import: "import_path",
853                         },
854                     ],
855                 },
856             },
857             {
858                 definition: {
859                     id: "#main/input_dir_array",
860                     type: {
861                         items: "Directory",
862                         type: "array",
863                     },
864                 },
865                 input: {
866                     input_dir_array: [
867                         {
868                             basename: "11111111111111111111111111111111+02",
869                             class: "Directory",
870                             location: "keep:11111111111111111111111111111111+02",
871                         },
872                         {
873                             basename: "11111111111111111111111111111111+03",
874                             class: "Directory",
875                             location: "keep:11111111111111111111111111111111+03",
876                         },
877                         {
878                             $import: "import_path",
879                         },
880                     ],
881                 },
882             },
883             {
884                 definition: {
885                     id: "#main/input_int_array",
886                     type: {
887                         items: "int",
888                         type: "array",
889                     },
890                 },
891                 input: {
892                     input_int_array: [
893                         1,
894                         3,
895                         5,
896                         {
897                             $import: "import_path",
898                         },
899                     ],
900                 },
901             },
902             {
903                 definition: {
904                     id: "#main/input_long_array",
905                     type: {
906                         items: "long",
907                         type: "array",
908                     },
909                 },
910                 input: {
911                     input_long_array: [
912                         10,
913                         20,
914                         {
915                             $import: "import_path",
916                         },
917                     ],
918                 },
919             },
920             {
921                 definition: {
922                     id: "#main/input_float_array",
923                     type: {
924                         items: "float",
925                         type: "array",
926                     },
927                 },
928                 input: {
929                     input_float_array: [
930                         10.2,
931                         10.4,
932                         10.6,
933                         {
934                             $import: "import_path",
935                         },
936                     ],
937                 },
938             },
939             {
940                 definition: {
941                     id: "#main/input_double_array",
942                     type: {
943                         items: "double",
944                         type: "array",
945                     },
946                 },
947                 input: {
948                     input_double_array: [
949                         20.1,
950                         20.2,
951                         20.3,
952                         {
953                             $import: "import_path",
954                         },
955                     ],
956                 },
957             },
958             {
959                 definition: {
960                     id: "#main/input_string_array",
961                     type: {
962                         items: "string",
963                         type: "array",
964                     },
965                 },
966                 input: {
967                     input_string_array: [
968                         "Hello",
969                         "World",
970                         "!",
971                         {
972                             $import: "import_path",
973                         },
974                     ],
975                 },
976             },
977             {
978                 definition: {
979                     id: "#main/input_bool_include",
980                     type: "boolean",
981                 },
982                 input: {
983                     input_bool_include: {
984                         $include: "include_path",
985                     },
986                 },
987             },
988             {
989                 definition: {
990                     id: "#main/input_int_include",
991                     type: "int",
992                 },
993                 input: {
994                     input_int_include: {
995                         $include: "include_path",
996                     },
997                 },
998             },
999             {
1000                 definition: {
1001                     id: "#main/input_float_include",
1002                     type: "float",
1003                 },
1004                 input: {
1005                     input_float_include: {
1006                         $include: "include_path",
1007                     },
1008                 },
1009             },
1010             {
1011                 definition: {
1012                     id: "#main/input_string_include",
1013                     type: "string",
1014                 },
1015                 input: {
1016                     input_string_include: {
1017                         $include: "include_path",
1018                     },
1019                 },
1020             },
1021             {
1022                 definition: {
1023                     id: "#main/input_file_include",
1024                     type: "File",
1025                 },
1026                 input: {
1027                     input_file_include: {
1028                         $include: "include_path",
1029                     },
1030                 },
1031             },
1032             {
1033                 definition: {
1034                     id: "#main/input_directory_include",
1035                     type: "Directory",
1036                 },
1037                 input: {
1038                     input_directory_include: {
1039                         $include: "include_path",
1040                     },
1041                 },
1042             },
1043             {
1044                 definition: {
1045                     id: "#main/input_file_url",
1046                     type: "File",
1047                 },
1048                 input: {
1049                     input_file_url: {
1050                         basename: "index.html",
1051                         class: "File",
1052                         location: "http://example.com/index.html",
1053                     },
1054                 },
1055             },
1056         ];
1057
1058         const testOutputs = [
1059             {
1060                 definition: {
1061                     id: "#main/output_file",
1062                     label: "Label Description",
1063                     type: "File",
1064                 },
1065                 output: {
1066                     output_file: {
1067                         basename: "cat.png",
1068                         class: "File",
1069                         location: "cat.png",
1070                     },
1071                 },
1072             },
1073             {
1074                 definition: {
1075                     id: "#main/output_file_with_secondary",
1076                     doc: "Doc Description",
1077                     type: "File",
1078                 },
1079                 output: {
1080                     output_file_with_secondary: {
1081                         basename: "main.dat",
1082                         class: "File",
1083                         location: "main.dat",
1084                         secondaryFiles: [
1085                             {
1086                                 basename: "secondary.dat",
1087                                 class: "File",
1088                                 location: "secondary.dat",
1089                             },
1090                             {
1091                                 basename: "secondary2.dat",
1092                                 class: "File",
1093                                 location: "secondary2.dat",
1094                             },
1095                         ],
1096                     },
1097                 },
1098             },
1099             {
1100                 definition: {
1101                     id: "#main/output_dir",
1102                     doc: ["Doc desc 1", "Doc desc 2"],
1103                     type: "Directory",
1104                 },
1105                 output: {
1106                     output_dir: {
1107                         basename: "outdir1",
1108                         class: "Directory",
1109                         location: "outdir1",
1110                     },
1111                 },
1112             },
1113             {
1114                 definition: {
1115                     id: "#main/output_bool",
1116                     type: "boolean",
1117                 },
1118                 output: {
1119                     output_bool: true,
1120                 },
1121             },
1122             {
1123                 definition: {
1124                     id: "#main/output_int",
1125                     type: "int",
1126                 },
1127                 output: {
1128                     output_int: 1,
1129                 },
1130             },
1131             {
1132                 definition: {
1133                     id: "#main/output_long",
1134                     type: "long",
1135                 },
1136                 output: {
1137                     output_long: 1,
1138                 },
1139             },
1140             {
1141                 definition: {
1142                     id: "#main/output_float",
1143                     type: "float",
1144                 },
1145                 output: {
1146                     output_float: 100.5,
1147                 },
1148             },
1149             {
1150                 definition: {
1151                     id: "#main/output_double",
1152                     type: "double",
1153                 },
1154                 output: {
1155                     output_double: 100.3,
1156                 },
1157             },
1158             {
1159                 definition: {
1160                     id: "#main/output_string",
1161                     type: "string",
1162                 },
1163                 output: {
1164                     output_string: "Hello output",
1165                 },
1166             },
1167             {
1168                 definition: {
1169                     id: "#main/output_file_array",
1170                     type: {
1171                         items: "File",
1172                         type: "array",
1173                     },
1174                 },
1175                 output: {
1176                     output_file_array: [
1177                         {
1178                             basename: "output2.tar",
1179                             class: "File",
1180                             location: "output2.tar",
1181                         },
1182                         {
1183                             basename: "output3.tar",
1184                             class: "File",
1185                             location: "output3.tar",
1186                         },
1187                     ],
1188                 },
1189             },
1190             {
1191                 definition: {
1192                     id: "#main/output_dir_array",
1193                     type: {
1194                         items: "Directory",
1195                         type: "array",
1196                     },
1197                 },
1198                 output: {
1199                     output_dir_array: [
1200                         {
1201                             basename: "outdir2",
1202                             class: "Directory",
1203                             location: "outdir2",
1204                         },
1205                         {
1206                             basename: "outdir3",
1207                             class: "Directory",
1208                             location: "outdir3",
1209                         },
1210                     ],
1211                 },
1212             },
1213             {
1214                 definition: {
1215                     id: "#main/output_int_array",
1216                     type: {
1217                         items: "int",
1218                         type: "array",
1219                     },
1220                 },
1221                 output: {
1222                     output_int_array: [10, 11, 12],
1223                 },
1224             },
1225             {
1226                 definition: {
1227                     id: "#main/output_long_array",
1228                     type: {
1229                         items: "long",
1230                         type: "array",
1231                     },
1232                 },
1233                 output: {
1234                     output_long_array: [51, 52],
1235                 },
1236             },
1237             {
1238                 definition: {
1239                     id: "#main/output_float_array",
1240                     type: {
1241                         items: "float",
1242                         type: "array",
1243                     },
1244                 },
1245                 output: {
1246                     output_float_array: [100.2, 100.4, 100.6],
1247                 },
1248             },
1249             {
1250                 definition: {
1251                     id: "#main/output_double_array",
1252                     type: {
1253                         items: "double",
1254                         type: "array",
1255                     },
1256                 },
1257                 output: {
1258                     output_double_array: [100.1, 100.2, 100.3],
1259                 },
1260             },
1261             {
1262                 definition: {
1263                     id: "#main/output_string_array",
1264                     type: {
1265                         items: "string",
1266                         type: "array",
1267                     },
1268                 },
1269                 output: {
1270                     output_string_array: ["Hello", "Output", "!"],
1271                 },
1272             },
1273         ];
1274
1275         const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
1276             cy.get("table tr")
1277                 .contains(name)
1278                 .parents("tr")
1279                 .within($mainRow => {
1280                     cy.get($mainRow).scrollIntoView();
1281                     label && cy.contains(label);
1282
1283                     if (multipleRows) {
1284                         cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
1285                         if (val) {
1286                             if (Array.isArray(val)) {
1287                                 val.forEach(v => cy.get("@secondaryRows").contains(v));
1288                             } else {
1289                                 cy.get("@secondaryRows").contains(val);
1290                             }
1291                         }
1292                         if (collection) {
1293                             cy.get("@secondaryRows").contains(collection);
1294                         }
1295                     } else {
1296                         if (val) {
1297                             if (Array.isArray(val)) {
1298                                 val.forEach(v => cy.contains(v));
1299                             } else {
1300                                 cy.contains(val);
1301                             }
1302                         }
1303                         if (collection) {
1304                             cy.contains(collection);
1305                         }
1306                     }
1307                 });
1308         };
1309
1310         const verifyIOParameterImage = (name, url) => {
1311             cy.get("table tr")
1312                 .contains(name)
1313                 .parents("tr")
1314                 .within(() => {
1315                     cy.get('[alt="Inline Preview"]')
1316                         .should("be.visible")
1317                         .and($img => {
1318                             expect($img[0].naturalWidth).to.be.greaterThan(0);
1319                             expect($img[0].src).contains(url);
1320                         });
1321                 });
1322         };
1323
1324         it("displays IO parameters with keep links and previews", function () {
1325             // Create output collection for real files
1326             cy.createCollection(adminUser.token, {
1327                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
1328                 owner_uuid: activeUser.user.uuid,
1329             }).then(testOutputCollection => {
1330                 cy.loginAs(activeUser);
1331
1332                 cy.goToPath(`/collections/${testOutputCollection.uuid}`);
1333
1334                 cy.get("[data-cy=upload-button]").click();
1335
1336                 cy.fixture("files/cat.png", "base64").then(content => {
1337                     cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
1338                     cy.get("[data-cy=form-submit-btn]").click();
1339                     cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
1340                     // Confirm final collection state.
1341                     cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
1342                 });
1343
1344                 cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
1345             });
1346
1347             // Get updated collection pdh
1348             cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
1349                 // Add output uuid and inputs to container request
1350                 cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1351                     req.on('response', res => {
1352                         res.body.output_uuid = testOutputCollection.uuid;
1353                         res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1354                             content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
1355                         };
1356                         res.body.mounts["/var/lib/cwl/workflow.json"] = {
1357                             content: {
1358                                 $graph: [
1359                                     {
1360                                         id: "#main",
1361                                         inputs: testInputs.map(input => input.definition),
1362                                         outputs: testOutputs.map(output => output.definition),
1363                                     },
1364                                 ],
1365                             },
1366                         };
1367                     });
1368                 });
1369
1370                 // Stub fake output collection
1371                 cy.intercept(
1372                     { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
1373                     {
1374                         statusCode: 200,
1375                         body: {
1376                             uuid: testOutputCollection.uuid,
1377                             portable_data_hash: testOutputCollection.portable_data_hash,
1378                         },
1379                     }
1380                 );
1381
1382                 // Stub fake output json
1383                 cy.intercept(
1384                     { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
1385                     {
1386                         statusCode: 200,
1387                         body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
1388                     }
1389                 );
1390
1391                 // Stub webdav response, points to output json
1392                 cy.intercept(
1393                     { method: "PROPFIND", url: "*" },
1394                     {
1395                         fixture: "webdav-propfind-outputs.xml",
1396                     }
1397                 );
1398             });
1399
1400             createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1401                 "containerRequest"
1402             );
1403
1404             cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
1405                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1406                 cy.get("[data-cy=process-io-card] h6")
1407                     .contains("Input Parameters")
1408                     .parents("[data-cy=process-io-card]")
1409                     .within((ctx) => {
1410                         cy.get(ctx).scrollIntoView();
1411                         verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
1412                         verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
1413                         verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
1414                         verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
1415                         verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
1416                         verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
1417                         verifyIOParameter("input_int", null, null, "1");
1418                         verifyIOParameter("input_long", null, null, "1");
1419                         verifyIOParameter("input_float", null, null, "1.5");
1420                         verifyIOParameter("input_double", null, null, "1.3");
1421                         verifyIOParameter("input_string", null, null, "Hello World");
1422                         verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
1423                         verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
1424                         verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
1425                         verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
1426                         verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
1427                         verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
1428                         verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
1429                         verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
1430                         verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
1431                         verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
1432                         verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
1433                         verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
1434                         verifyIOParameter("input_bool_include", null, null, "Cannot display value");
1435                         verifyIOParameter("input_int_include", null, null, "Cannot display value");
1436                         verifyIOParameter("input_float_include", null, null, "Cannot display value");
1437                         verifyIOParameter("input_string_include", null, null, "Cannot display value");
1438                         verifyIOParameter("input_file_include", null, null, "Cannot display value");
1439                         verifyIOParameter("input_directory_include", null, null, "Cannot display value");
1440                         verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
1441                     });
1442                 cy.get("[data-cy=process-io-card] h6")
1443                     .contains("Output Parameters")
1444                     .parents("[data-cy=process-io-card]")
1445                     .within(ctx => {
1446                         cy.get(ctx).scrollIntoView();
1447                         const outPdh = testOutputCollection.portable_data_hash;
1448
1449                         verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
1450                         // Disabled until image preview returns
1451                         // verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
1452                         verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
1453                         verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
1454                         verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
1455                         verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
1456                         verifyIOParameter("output_bool", null, null, "true");
1457                         verifyIOParameter("output_int", null, null, "1");
1458                         verifyIOParameter("output_long", null, null, "1");
1459                         verifyIOParameter("output_float", null, null, "100.5");
1460                         verifyIOParameter("output_double", null, null, "100.3");
1461                         verifyIOParameter("output_string", null, null, "Hello output");
1462                         verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
1463                         verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
1464                         verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
1465                         verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
1466                         verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
1467                         verifyIOParameter("output_long_array", null, null, ["51", "52"]);
1468                         verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
1469                         verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
1470                         verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
1471                     });
1472             });
1473         });
1474
1475         it("displays IO parameters with no value", function () {
1476             const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
1477             const fakeOutputPDH = "11111111111111111111111111111111+99/";
1478
1479             cy.loginAs(activeUser);
1480
1481             // Add output uuid and inputs to container request
1482             cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
1483                 req.on('response', res => {
1484                     res.body.output_uuid = fakeOutputUUID;
1485                     res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
1486                         content: {},
1487                     };
1488                     res.body.mounts["/var/lib/cwl/workflow.json"] = {
1489                         content: {
1490                             $graph: [
1491                                 {
1492                                     id: "#main",
1493                                     inputs: testInputs.map(input => input.definition),
1494                                     outputs: testOutputs.map(output => output.definition),
1495                                 },
1496                             ],
1497                         },
1498                     };
1499                 });
1500             });
1501
1502             // Stub fake output collection
1503             cy.intercept(
1504                 { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
1505                 {
1506                     statusCode: 200,
1507                     body: {
1508                         uuid: fakeOutputUUID,
1509                         portable_data_hash: fakeOutputPDH,
1510                     },
1511                 }
1512             );
1513
1514             // Stub fake output json
1515             cy.intercept(
1516                 { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
1517                 {
1518                     statusCode: 200,
1519                     body: {},
1520                 }
1521             );
1522
1523             cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
1524                 // Stub webdav response, points to output json
1525                 cy.intercept(
1526                     { method: "PROPFIND", url: "*" },
1527                     {
1528                         statusCode: 200,
1529                         body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
1530                     }
1531                 );
1532             });
1533
1534             createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
1535                 "containerRequest"
1536             );
1537
1538             cy.getAll("@containerRequest").then(function ([containerRequest]) {
1539                 cy.goToPath(`/processes/${containerRequest.uuid}`);
1540                 cy.waitForDom();
1541
1542                 cy.get("[data-cy=process-io-card] h6")
1543                     .contains("Input Parameters")
1544                     .parents("[data-cy=process-io-card]")
1545                     .within((ctx) => {
1546                         cy.get(ctx).scrollIntoView();
1547                         cy.wait(2000);
1548                         cy.waitForDom();
1549
1550                         testInputs.map((input) => {
1551                             verifyIOParameter(input.definition.id.split('/').slice(-1)[0], null, null, "No value");
1552                         });
1553                     });
1554                 cy.get("[data-cy=process-io-card] h6")
1555                     .contains("Output Parameters")
1556                     .parents("[data-cy=process-io-card]")
1557                     .within((ctx) => {
1558                         cy.get(ctx).scrollIntoView();
1559
1560                         testOutputs.map((output) => {
1561                             verifyIOParameter(output.definition.id.split('/').slice(-1)[0], null, null, "No value");
1562                         });
1563                     });
1564             });
1565         });
1566     });
1567 });