Skip to content

Commit

Permalink
Merge pull request #3599 from kbase/UIP-45-add-text-only-output
Browse files Browse the repository at this point in the history
UIP-45 add text only output
  • Loading branch information
briehl authored Jul 16, 2024
2 parents 949dac9 + d0d493c commit 1d2b3d7
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 9 deletions.
6 changes: 2 additions & 4 deletions nbextensions/appCell2/widgets/appCellWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,6 @@ define(
AppParamsWidget,
AppParamsViewWidget
) => {
'use strict';

const t = html.tag,
div = t('div'),
span = t('span'),
Expand Down Expand Up @@ -1301,7 +1299,8 @@ define(
skip the output cell creation.
*/
// widgets named 'no-display' are a trigger to skip the output cell process.
const skipOutputCell = model.getItem('exec.outputWidgetInfo.name') === 'no-display';
const widgetName = model.getItem('exec.outputWidgetInfo.name');
const skipOutputCell = widgetName === 'no-display' || widgetName === 'text-only';
let cellInfo;
if (skipOutputCell) {
cellInfo = {
Expand Down Expand Up @@ -1834,7 +1833,6 @@ define(
};
},
(err) => {
'use strict';
console.error('ERROR loading appCell appCellWidget', err);
}
);
49 changes: 46 additions & 3 deletions nbextensions/appCell2/widgets/tabs/resultsViewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ define([
'kb_service/client/narrativeMethodStore',
'common/html',
'util/display',
'util/string',
'kbaseReportView',
], (Promise, $, UI, Runtime, Events, NarrativeMethodStore, html, DisplayUtil, KBaseReportView) => {
'use strict';
], (
Promise,
$,
UI,
Runtime,
Events,
NarrativeMethodStore,
html,
DisplayUtil,
StringUtil,
KBaseReportView
) => {
const t = html.tag,
div = t('div'),
a = t('a'),
Expand Down Expand Up @@ -90,17 +101,49 @@ define([
// the job output
if (!reportInputs) {
return Promise.try(() => {
const viewerName = result ? result.name : null;
const jobOutput = jobState.job_output
? jobState.job_output.result
: 'no output found';
if (viewerName === 'text-only') {
ui.setContent('results.body', buildOutputText(result.params.result_text));
} else {
ui.setContent('results.body', ui.buildPresentableJson(jobOutput));
}
ui.getElement('results').classList.remove('hidden');
ui.setContent('results.body', ui.buildPresentableJson(jobOutput));
});
}
// otherwise, render the report
return renderReportView(reportInputs);
}

/**
* Expects that the result text should be a string, but handles other cases as well.
* If resultText is not a string or number, or is an empty string, this sets the text
* to "App completed successfully." Otherwise, it truncates the result to 1000 characters.
*
* This then gets HTML-escaped and embedded in a div for returning.
* @param {str} resultText
* @returns
*/
function buildOutputText(resultText) {
// default if text is empty, null, or undefined
if (
!(typeof resultText === 'string' && resultText.trim().length > 0) &&
typeof resultText !== 'number'
) {
resultText = 'App completed successfully.';
}
resultText = String(resultText).trim();
// cap the results at 1,000 characters.
if (resultText.length > 1000) {
resultText =
resultText.substring(0, 1000) +
` [truncated from ${resultText.length} characters]`;
}
return div(StringUtil.escape(resultText));
}

function lazyRenderReport() {
return Promise.try(() => {
const nbContainer = document.querySelector('#notebook-container');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ define([
'testUtil',
'narrativeMocks',
], (ResultsViewer, Props, Jupyter, Config, TestUtil, Mocks) => {
'use strict';

const mockModelData = {
exec: {},
app: {
Expand All @@ -23,6 +21,7 @@ define([
};

const mockOutputWidgetInfo = {
name: 'no-display',
params: {
report_name: 'some_report',
report_ref: '1/2/3',
Expand Down Expand Up @@ -57,6 +56,31 @@ define([
return Props.make({ data });
}

/**
* Builds and returns a mock data model (Props object) and final job state.
* @param {string} text - if present, this gets used instead of a random string
* @returns
*/
function buildTextModel(resultText) {
const data = TestUtil.JSONcopy(mockModelData);
const outputInfo = {
name: 'text-only',
params: {
result_text: resultText,
},
};
const jobState = {
job_output: {
result: [outputInfo],
},
};
data.exec.outputWidgetInfo = outputInfo;
return {
model: Props.make({ data }),
jobState,
};
}

describe('App Cell Results Viewer tests', () => {
let node, outerNode;

Expand Down Expand Up @@ -123,6 +147,63 @@ define([
}
});

it('starts a viewer with text output', async () => {
const resultText = 'ABCDEFGHIJKL';
const { model, jobState } = buildTextModel(resultText);
const viewer = ResultsViewer.make({ model });
await viewer.start({
node,
jobState,
isParentJob: false,
});
expect(node.querySelector('.kb-app-results-tab')).toBeDefined();
expect(node.innerHTML).toContain(resultText);
});

it('starts a viewer with truncated output', async () => {
const textLength = 10000;
const resultText = 'A'.repeat(textLength);
const maxLen = 1000;
const { model, jobState } = buildTextModel(resultText);
const viewer = ResultsViewer.make({ model });
await viewer.start({
node,
jobState,
isParentJob: false,
});
expect(node.querySelector('.kb-app-results-tab')).toBeDefined();
expect(node.innerHTML).toContain(resultText.substring(0, maxLen));
expect(node.innerHTML).toContain(`[truncated from ${textLength} characters]`);
});

const defaultCases = [null, undefined, {}, [], '', ' '];
defaultCases.forEach((testCase) =>
it(`creates a text viewer with default success for input ${testCase}`, async () => {
const { model, jobState } = buildTextModel(testCase);
const viewer = ResultsViewer.make({ model });
await viewer.start({
node,
jobState,
isParentJob: false,
});
expect(node.querySelector('.kb-app-results-tab')).toBeDefined();
expect(node.innerHTML).toContain('App completed successfully.');
})
);

it('creates a text viewer that escapes html', async () => {
const resultText = '<script>alert("this failed!")</script>';
const { model, jobState } = buildTextModel(resultText);
const viewer = ResultsViewer.make({ model });
await viewer.start({
node,
jobState,
isParentJob: false,
});
expect(node.querySelector('.kb-app-results-tab')).toBeDefined();
expect(node.innerHTML).toContain('&lt;script&gt;alert');
});

it('starts and stops a viewer with a report from a batch job', async () => {
const model = buildModel({ hasReport: true, isParentJob: true, jobComplete: true });
const viewer = ResultsViewer.make({ model });
Expand Down

0 comments on commit 1d2b3d7

Please sign in to comment.