diff --git a/client/package-lock.json b/client/package-lock.json index f000bf735..530f88263 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -76,6 +76,7 @@ "stylelint": "^14.4.0", "stylelint-config-recommended": "^7.0.0", "stylelint-config-standard": "^25.0.0", + "timekeeper": "^2.3.1", "typescript": "^4.4.3", "vite": "^2.9.16", "weak-key": "^1.0.1" @@ -14617,6 +14618,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "node_modules/timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true + }, "node_modules/tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", @@ -25859,6 +25866,12 @@ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, + "timekeeper": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/timekeeper/-/timekeeper-2.3.1.tgz", + "integrity": "sha512-LeQRS7/4JcC0PgdSFnfUiStQEdiuySlCj/5SJ18D+T1n9BoY7PxKFfCwLulpHXoLUFr67HxBddQdEX47lDGx1g==", + "dev": true + }, "tiny-invariant": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", diff --git a/client/package.json b/client/package.json index db805dab0..8313ccab5 100644 --- a/client/package.json +++ b/client/package.json @@ -98,6 +98,7 @@ "stylelint": "^14.4.0", "stylelint-config-recommended": "^7.0.0", "stylelint-config-standard": "^25.0.0", + "timekeeper": "^2.3.1", "typescript": "^4.4.3", "vite": "^2.9.16", "weak-key": "^1.0.1" diff --git a/client/src/components/Applications/AppForm/AppForm.jsx b/client/src/components/Applications/AppForm/AppForm.jsx index 787dcf255..0667f13aa 100644 --- a/client/src/components/Applications/AppForm/AppForm.jsx +++ b/client/src/components/Applications/AppForm/AppForm.jsx @@ -484,6 +484,7 @@ export const AppSchemaForm = ({ app }) => { targetDir: isTargetPathField(k) ? v : null, }; }) + .filter((v) => v) //filter nulls .reduce((acc, entry) => { // merge input field and targetPath fields into one. const key = getInputFieldFromTargetPathField(entry.name); diff --git a/client/src/components/Applications/AppForm/AppForm.test.js b/client/src/components/Applications/AppForm/AppForm.test.js index d09920342..00ed86b1f 100644 --- a/client/src/components/Applications/AppForm/AppForm.test.js +++ b/client/src/components/Applications/AppForm/AppForm.test.js @@ -1,5 +1,6 @@ import React from 'react'; -import { render, waitFor } from '@testing-library/react'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; import { Provider } from 'react-redux'; import configureStore from 'redux-mock-store'; import { BrowserRouter } from 'react-router-dom'; @@ -15,11 +16,16 @@ import { appTrayExpectedFixture, } from '../../../redux/sagas/fixtures/apptray.fixture'; import { initialAppState } from '../../../redux/reducers/apps.reducers'; -import { helloWorldAppFixture } from './fixtures/AppForm.app.fixture'; +import { + helloWorldAppFixture, + helloWorldAppSubmissionPayloadFixture, +} from './fixtures/AppForm.app.fixture'; import systemsFixture from '../../DataFiles/fixtures/DataFiles.systems.fixture'; import { projectsFixture } from '../../../redux/sagas/fixtures/projects.fixture'; import '@testing-library/jest-dom/extend-expect'; +import timekeeper from 'timekeeper'; +const frozenDate = '2023-10-01'; const mockStore = configureStore(); const initialMockState = { allocations: allocationsFixture, @@ -56,6 +62,11 @@ function renderAppSchemaFormComponent(store, app) { } describe('AppSchemaForm', () => { + beforeAll(() => { + // Lock Time + timekeeper.freeze(new Date(frozenDate)); + }); + it('renders the AppSchemaForm', async () => { const store = mockStore({ ...initialMockState, @@ -257,6 +268,162 @@ describe('AppSchemaForm', () => { expect(getByText(/Activate your Application Name license/)).toBeDefined(); }); }); + + it('job submission with file input mode FIXED', async () => { + const store = mockStore({ + ...initialMockState, + }); + + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + jobAttributes: { + ...helloWorldAppFixture.definition.jobAttributes, + fileInputs: [ + { + name: 'File to copy', + description: 'A fixed file used by the app', + inputMode: 'FIXED', + autoMountLocal: true, + sourceUrl: + 'tapis://corral-tacc/tacc/aci/secure-test/rallyGolf.jpg', + targetPath: 'rallyGolf.jpg', + }, + ], + }, + }, + }); + const hiddenFileInput = container.querySelector( + 'input[name="fileInputs.File to copy"]' + ); + // FIXED fields are still shown in UI but not submitted. + expect(hiddenFileInput).toBeInTheDocument(); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + it('job submission with file input hidden', async () => { + const store = mockStore({ + ...initialMockState, + }); + + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + jobAttributes: { + ...helloWorldAppFixture.definition.jobAttributes, + fileInputs: [ + { + name: 'File to copy', + description: 'A fixed file used by the app', + inputMode: 'REQUIRED', + autoMountLocal: true, + sourceUrl: + 'tapis://corral-tacc/tacc/aci/secure-test/rallyGolf.jpg', + targetPath: 'rallyGolf.jpg', + notes: { + isHidden: true, + }, + }, + ], + }, + }, + }); + + const hiddenFileInput = container.querySelector( + 'input[name="fileInputs.File to copy"]' + ); + expect(hiddenFileInput).not.toBeInTheDocument(); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + it('job submission with custom target path', async () => { + const store = mockStore({ + ...initialMockState, + }); + const { getByText, container } = renderAppSchemaFormComponent(store, { + ...helloWorldAppFixture, + definition: { + ...helloWorldAppFixture.definition, + notes: { + ...helloWorldAppFixture.definition.notes, + showTargetPath: true, + }, + }, + }); + + const fileInput = container.querySelector( + 'input[name="fileInputs.File to modify"]' + ); + const file = 'tapis://foo/bar.txt'; + const targetPathForFile = 'baz.txt'; + fireEvent.change(fileInput, { target: { value: file } }); + const targetPathInput = container.querySelector( + 'input[name="fileInputs._TargetPath_File to modify"]' + ); + fireEvent.change(targetPathInput, { target: { value: targetPathForFile } }); + + const submitButton = getByText(/Submit/); + fireEvent.click(submitButton); + const payload = { + ...helloWorldAppSubmissionPayloadFixture, + job: { + ...helloWorldAppSubmissionPayloadFixture.job, + fileInputs: [ + { + name: 'File to modify', + sourceUrl: file, + targetPath: targetPathForFile, + }, + ], + name: 'hello-world-0.0.1_' + frozenDate + 'T00:00:00', + }, + }; + + await waitFor(() => { + expect(store.getActions()).toEqual([ + { type: 'GET_SYSTEM_MONITOR' }, + { type: 'SUBMIT_JOB', payload: payload }, + ]); + }); + }); + + afterAll(() => { + timekeeper.reset(); + }); }); describe('AppDetail', () => { diff --git a/client/src/components/Applications/AppForm/AppFormSchema.js b/client/src/components/Applications/AppForm/AppFormSchema.js index cfd772f48..b3f26ba09 100644 --- a/client/src/components/Applications/AppForm/AppFormSchema.js +++ b/client/src/components/Applications/AppForm/AppFormSchema.js @@ -106,11 +106,10 @@ const FormSchema = (app) => { app.definition.notes.showTargetPath ?? false; (app.definition.jobAttributes.fileInputs || []).forEach((i) => { const input = i; - /* TODOv3 consider hidden file inputs https://jira.tacc.utexas.edu/browse/WP-102 - if (input.name.startsWith('_') || !input.value.visible) { // TODOv3 visible or hidden - return; - } - */ + if (input.notes?.isHidden) { + return; + } + const field = { label: input.name, description: input.description, diff --git a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js index 5aa718ba4..1458448ea 100644 --- a/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js +++ b/client/src/components/Applications/AppForm/fixtures/AppForm.app.fixture.js @@ -237,3 +237,49 @@ export const helloWorldAppFixture = { type: null, }, }; + +export const helloWorldAppSubmissionPayloadFixture = { + job: { + fileInputs: [], + parameterSet: { + appArgs: [ + { + name: 'Greeting', + arg: 'hello', + }, + { + name: 'Target', + arg: 'world', + }, + { + name: 'Sleep Time', + arg: '30', + }, + ], + containerArgs: [], + schedulerOptions: [ + { + name: 'TACC Allocation', + description: 'The TACC allocation associated with this job execution', + include: true, + arg: '-A TACC-ACI', + }, + ], + envVariables: [], + }, + name: 'hello-world-0.0.1', + nodeCount: 1, + coresPerNode: 1, + maxMinutes: 10, + archiveSystemId: 'frontera', + archiveSystemDir: + 'HOST_EVAL($HOME)/tapis-jobs-archive/${JobCreateDate}/${JobName}-${JobUUID}', + archiveOnAppError: true, + appId: 'hello-world', + appVersion: '0.0.1', + execSystemId: 'frontera', + execSystemLogicalQueue: 'development', + }, + licenseType: null, + isInteractive: false, +};