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