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