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