mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
v13 preparations: Standardize command data output
Change-type: patch Signed-off-by: Scott Lowe <scott@balena.io>
This commit is contained in:
parent
c125e0b38d
commit
f3fb9b6bdf
@ -21,6 +21,7 @@ import {
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
@ -167,4 +168,7 @@ export default abstract class BalenaCommand extends Command {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
}
|
||||
|
@ -28,8 +28,10 @@ import {
|
||||
appToFleetCmdMsg,
|
||||
warnify,
|
||||
} from '../../utils/messages';
|
||||
import { isV13 } from '../../utils/version';
|
||||
import type { DataOutputOptions } from '../../framework';
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataOutputOptions {
|
||||
help: void;
|
||||
}
|
||||
|
||||
@ -56,13 +58,14 @@ export class FleetCmd extends Command {
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
help: cf.help,
|
||||
...(isV13() ? cf.dataOutputFlags : {}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
public static primary = true;
|
||||
|
||||
public async run(parserOutput?: ParserOutput<FlagsDef, ArgsDef>) {
|
||||
const { args: params } =
|
||||
const { args: params, flags: options } =
|
||||
parserOutput || this.parse<FlagsDef, ArgsDef>(FleetCmd);
|
||||
|
||||
const { getApplication } = await import('../../utils/sdk');
|
||||
@ -82,16 +85,24 @@ export class FleetCmd extends Command {
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
application.commit = application.should_be_running__release[0]?.commit;
|
||||
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
if (isV13()) {
|
||||
await this.outputData(
|
||||
application,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${application.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(application, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,18 +17,20 @@
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
import type { Output as ParserOutput } from '@oclif/parser';
|
||||
|
||||
import Command from '../command';
|
||||
import * as cf from '../utils/common-flags';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../utils/lazy';
|
||||
import { appToFleetCmdMsg, warnify } from '../utils/messages';
|
||||
import { isV13 } from '../utils/version';
|
||||
import type { DataSetOutputOptions } from '../framework';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceType {
|
||||
device_count?: number;
|
||||
online_devices?: number;
|
||||
device_count: number;
|
||||
online_devices: number;
|
||||
device_type?: string;
|
||||
}
|
||||
|
||||
interface FlagsDef {
|
||||
interface FlagsDef extends DataSetOutputOptions {
|
||||
help: void;
|
||||
verbose?: boolean;
|
||||
}
|
||||
@ -48,12 +50,17 @@ export class FleetsCmd extends Command {
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags: flags.Input<FlagsDef> = {
|
||||
...(isV13()
|
||||
? {}
|
||||
: {
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'No-op since release v12.0.0',
|
||||
}),
|
||||
}),
|
||||
...(isV13() ? cf.dataSetOutputFlags : {}),
|
||||
help: cf.help,
|
||||
verbose: flags.boolean({
|
||||
default: false,
|
||||
char: 'v',
|
||||
description: 'No-op since release v12.0.0',
|
||||
}),
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -75,28 +82,39 @@ export class FleetsCmd extends Command {
|
||||
},
|
||||
})) as ExtendedApplication[];
|
||||
|
||||
const _ = await import('lodash');
|
||||
// Add extended properties
|
||||
applications.forEach((application) => {
|
||||
application.device_count = application.owns__device?.length ?? 0;
|
||||
application.online_devices = _.sumBy(application.owns__device, (d) =>
|
||||
d.is_online === true ? 1 : 0,
|
||||
);
|
||||
// @ts-expect-error
|
||||
application.online_devices =
|
||||
application.owns__device?.filter((d) => d.is_online).length || 0;
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
// Display
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
if (isV13()) {
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
'id',
|
||||
'app_name',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
_parserOutput.flags,
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applications, [
|
||||
'id',
|
||||
this.useAppWord ? 'app_name' : 'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'online_devices',
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
19
lib/framework/index.ts
Normal file
19
lib/framework/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from './output';
|
||||
|
||||
export { DataOutputOptions, DataSetOutputOptions };
|
158
lib/framework/output.ts
Normal file
158
lib/framework/output.ts
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright 2020 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getCliUx, getChalk } from '../utils/lazy';
|
||||
|
||||
export interface DataOutputOptions {
|
||||
fields?: string;
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
export interface DataSetOutputOptions extends DataOutputOptions {
|
||||
filter?: string;
|
||||
'no-header'?: boolean;
|
||||
'no-truncate'?: boolean;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output message to STDERR
|
||||
*/
|
||||
export function outputMessage(msg: string) {
|
||||
// Messages go to STDERR
|
||||
console.error(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output result data to STDOUT
|
||||
* Supports:
|
||||
* - arrays of items (displayed in a tabular way),
|
||||
* - single items (displayed in a field per row format).
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
export async function outputData(
|
||||
data: any[] | {},
|
||||
fields: string[],
|
||||
options: DataOutputOptions | DataSetOutputOptions,
|
||||
) {
|
||||
if (Array.isArray(data)) {
|
||||
await outputDataSet(data, fields, options as DataSetOutputOptions);
|
||||
} else {
|
||||
await outputDataItem(data, fields, options as DataOutputOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the cli.ux table implementation, to output tabular data
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataSet(
|
||||
data: any[],
|
||||
fields: string[],
|
||||
options: DataSetOutputOptions,
|
||||
) {
|
||||
// Oclif expects fields to be specified in the format used in table headers (though lowercase)
|
||||
// By replacing underscores with spaces here, we can support both header format and actual field name
|
||||
// (e.g. as seen in json output).
|
||||
options.fields = options.fields?.replace(/_/g, ' ');
|
||||
options.filter = options.filter?.replace(/_/g, ' ');
|
||||
options.sort = options.sort?.replace(/_/g, ' ');
|
||||
|
||||
getCliUx().table(
|
||||
data,
|
||||
// Convert fields array to column object keys
|
||||
// that cli.ux expects. We can later add support
|
||||
// for both formats if beneficial
|
||||
fields.reduce((ac, a) => ({ ...ac, [a]: {} }), {}),
|
||||
{
|
||||
...options,
|
||||
...(options.json
|
||||
? {
|
||||
output: 'json',
|
||||
}
|
||||
: {}),
|
||||
columns: options.fields,
|
||||
printLine,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a single data object (like `resin-cli-visuals table.vertical`),
|
||||
* but supporting a subset of options from `cli-ux table` (--json and --fields)
|
||||
*
|
||||
* @param data Array of data objects to output
|
||||
* @param fields Array of fieldnames, specifying the fields and display order
|
||||
* @param options Output options
|
||||
*/
|
||||
async function outputDataItem(
|
||||
data: any,
|
||||
fields: string[],
|
||||
options: DataOutputOptions,
|
||||
) {
|
||||
const outData: typeof data = {};
|
||||
|
||||
// Convert comma separated list of fields in `options.fields` to array of correct format.
|
||||
// Note, user may have specified the true field name (e.g. `some_field`),
|
||||
// or the format displayed in headers (e.g. `Some field`, case insensitive).
|
||||
const userSelectedFields = options.fields?.split(',').map((f) => {
|
||||
return f.toLowerCase().trim().replace(/ /g, '_');
|
||||
});
|
||||
|
||||
// Order and filter the fields based on `fields` parameter and `options.fields`
|
||||
(userSelectedFields || fields).forEach((fieldName) => {
|
||||
if (fields.includes(fieldName)) {
|
||||
outData[fieldName] = data[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
if (options.json) {
|
||||
printLine(JSON.stringify(outData, undefined, 2));
|
||||
} else {
|
||||
const chalk = getChalk();
|
||||
const { capitalize } = await import('lodash');
|
||||
|
||||
// Find longest key, so we can align results
|
||||
const longestKeyLength = getLongestObjectKeyLength(outData);
|
||||
|
||||
// Output one field per line
|
||||
for (const [k, v] of Object.entries(outData)) {
|
||||
const shim = ' '.repeat(longestKeyLength - k.length);
|
||||
const kDisplay = capitalize(k.replace(/_/g, ' '));
|
||||
printLine(`${chalk.bold(kDisplay) + shim} : ${v}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLongestObjectKeyLength(o: any): number {
|
||||
return Object.keys(o).length >= 1
|
||||
? Object.keys(o).reduce((a, b) => {
|
||||
return a.length > b.length ? a : b;
|
||||
}).length
|
||||
: 0;
|
||||
}
|
||||
|
||||
function printLine(s: any) {
|
||||
// Duplicating oclif cli-ux's default implementation here,
|
||||
// but using this one explicitly for ease of testing
|
||||
process.stdout.write(s + '\n');
|
||||
}
|
@ -16,11 +16,12 @@
|
||||
*/
|
||||
|
||||
import { flags } from '@oclif/command';
|
||||
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import { stripIndent } from './lazy';
|
||||
import { lowercaseIfSlug } from './normalization';
|
||||
|
||||
import { isV13 } from './version';
|
||||
import type { IBooleanFlag } from '@oclif/parser/lib/flags';
|
||||
import type { DataOutputOptions, DataSetOutputOptions } from '../framework';
|
||||
|
||||
export const v13: IBooleanFlag<boolean> = flags.boolean({
|
||||
description: stripIndent`\
|
||||
@ -125,3 +126,37 @@ export const json: IBooleanFlag<boolean> = flags.boolean({
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const dataOutputFlags: flags.Input<DataOutputOptions> = {
|
||||
fields: flags.string({
|
||||
description: 'only show provided fields (comma-separated)',
|
||||
}),
|
||||
json: flags.boolean({
|
||||
char: 'j',
|
||||
exclusive: ['no-truncate'],
|
||||
description: 'output in json format',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
export const dataSetOutputFlags: flags.Input<DataOutputOptions> &
|
||||
flags.Input<DataSetOutputOptions> = {
|
||||
...dataOutputFlags,
|
||||
filter: flags.string({
|
||||
description:
|
||||
'filter results by substring matching of a given field, eg: --filter field=foo',
|
||||
}),
|
||||
'no-header': flags.boolean({
|
||||
exclusive: ['json'],
|
||||
description: 'hide table header from output',
|
||||
default: false,
|
||||
}),
|
||||
'no-truncate': flags.boolean({
|
||||
exclusive: ['json'],
|
||||
description: 'do not truncate output to fit screen',
|
||||
default: false,
|
||||
}),
|
||||
sort: flags.string({
|
||||
description: `field to sort by (prepend '-' for descending order)`,
|
||||
}),
|
||||
};
|
||||
|
@ -220,6 +220,7 @@
|
||||
"chalk": "^3.0.0",
|
||||
"chokidar": "^3.4.3",
|
||||
"cli-truncate": "^2.1.0",
|
||||
"cli-ux": "^5.5.1",
|
||||
"color-hash": "^1.0.3",
|
||||
"columnify": "^1.5.2",
|
||||
"common-tags": "^1.7.2",
|
||||
|
255
tests/framework/output.spec.ts
Normal file
255
tests/framework/output.spec.ts
Normal file
@ -0,0 +1,255 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020-2021 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* tslint:disable: prefer-const no-empty */
|
||||
|
||||
import rewire = require('rewire');
|
||||
import sinon = require('sinon');
|
||||
import { expect } from 'chai';
|
||||
|
||||
const dataItem = {
|
||||
name: 'item1',
|
||||
id: 1,
|
||||
thing_color: 'blue',
|
||||
thing_shape: 'square',
|
||||
};
|
||||
|
||||
const dataSet = [
|
||||
{
|
||||
name: 'item1',
|
||||
id: 1,
|
||||
thing_color: 'red',
|
||||
thing_shape: 'square',
|
||||
},
|
||||
{
|
||||
name: 'item2',
|
||||
id: 2,
|
||||
thing_color: 'blue',
|
||||
thing_shape: 'round',
|
||||
},
|
||||
];
|
||||
|
||||
describe('outputData', function () {
|
||||
let outputData: any;
|
||||
let outputDataSetSpy: any;
|
||||
let outputDataItemSpy: any;
|
||||
|
||||
this.beforeEach(() => {
|
||||
const output = rewire('../../build/framework/output');
|
||||
|
||||
outputDataSetSpy = sinon.spy();
|
||||
outputDataItemSpy = sinon.spy();
|
||||
|
||||
output.__set__('outputDataSet', outputDataSetSpy);
|
||||
output.__set__('outputDataItem', outputDataItemSpy);
|
||||
|
||||
outputData = output.__get__('outputData');
|
||||
});
|
||||
|
||||
it('should call outputDataSet function when data param is an array', async () => {
|
||||
await outputData(dataSet);
|
||||
expect(outputDataSetSpy.called).to.be.true;
|
||||
expect(outputDataItemSpy.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should call outputDataItem function when data param is an object', async () => {
|
||||
await outputData(dataItem);
|
||||
expect(outputDataSetSpy.called).to.be.false;
|
||||
expect(outputDataItemSpy.called).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputDataSet', function () {
|
||||
let outputDataSet: any;
|
||||
let printLineSpy: any;
|
||||
|
||||
this.beforeEach(() => {
|
||||
const output = rewire('../../build/framework/output');
|
||||
printLineSpy = sinon.spy();
|
||||
output.__set__('printLine', printLineSpy);
|
||||
outputDataSet = output.__get__('outputDataSet');
|
||||
});
|
||||
|
||||
it('should only output fields specified in `fields` param, in that order', async () => {
|
||||
const fields = ['id', 'name', 'thing_color'];
|
||||
const options = {};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
// check correct number of rows (2 data, 2 header)
|
||||
expect(printLineSpy.callCount).to.equal(4);
|
||||
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||
// check we have fields we specified
|
||||
fields.forEach((f) => {
|
||||
expect(headerLine).to.include(f.replace(/_/g, ' '));
|
||||
});
|
||||
// check we don't have fields we didn't specify
|
||||
expect(headerLine).to.not.include('thing_shape');
|
||||
// check order
|
||||
// split header using the `name` column as delimiter
|
||||
const splitHeader = headerLine.split('name');
|
||||
expect(splitHeader[0]).to.include('id');
|
||||
expect(splitHeader[1]).to.include('thing');
|
||||
});
|
||||
|
||||
/*
|
||||
it('should output fields in the order specified in `fields` param', async () => {
|
||||
const fields = ['thing_color', 'id', 'name'];
|
||||
const options = {};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||
// split header using the `it` column as delimiter
|
||||
const splitHeader = headerLine.split('id');
|
||||
expect(splitHeader[0]).to.include('thing');
|
||||
expect(splitHeader[1]).to.include('name');
|
||||
});
|
||||
*/
|
||||
|
||||
it('should only output fields specified in `options.fields` if present', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
// test all formats
|
||||
fields: 'Name,thing_color,Thing shape',
|
||||
};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
const headerLine = printLineSpy.firstCall.firstArg.toLowerCase();
|
||||
// check we have fields we specified
|
||||
expect(headerLine).to.include('name');
|
||||
expect(headerLine).to.include('thing color');
|
||||
expect(headerLine).to.include('thing shape');
|
||||
// check we don't have fields we didn't specify
|
||||
expect(headerLine).to.not.include('id');
|
||||
});
|
||||
|
||||
it('should output records in order specified by `options.sort` if present', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
sort: 'thing shape',
|
||||
'no-header': true,
|
||||
};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
// blue should come before red
|
||||
expect(printLineSpy.getCall(0).firstArg).to.include('blue');
|
||||
expect(printLineSpy.getCall(1).firstArg).to.include('red');
|
||||
});
|
||||
|
||||
it('should only output records that match filter specified by `options.filter` if present', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
filter: 'thing color=red',
|
||||
'no-header': true,
|
||||
};
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
// check correct number of rows (1 matched data, no-header)
|
||||
expect(printLineSpy.callCount).to.equal(1);
|
||||
expect(printLineSpy.getCall(0).firstArg).to.include('red');
|
||||
});
|
||||
|
||||
it('should output data in json format, if `options.json` true', async () => {
|
||||
const fields = ['name', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
json: true,
|
||||
};
|
||||
|
||||
// TODO: I've run into an oclif cli-ux bug, where numbers are output as strings in json
|
||||
// (this can be seen by including 'id' in the fields list above).
|
||||
// Issue opened: https://github.com/oclif/cli-ux/issues/309
|
||||
// For now removing id for this test.
|
||||
const clonedDataSet = JSON.parse(JSON.stringify(dataSet));
|
||||
clonedDataSet.forEach((d: any) => {
|
||||
delete d.id;
|
||||
});
|
||||
|
||||
const expectedJson = JSON.stringify(clonedDataSet, undefined, 2);
|
||||
|
||||
await outputDataSet(dataSet, fields, options);
|
||||
|
||||
expect(printLineSpy.callCount).to.equal(1);
|
||||
expect(printLineSpy.getCall(0).firstArg).to.equal(expectedJson);
|
||||
});
|
||||
});
|
||||
|
||||
describe('outputDataItem', function () {
|
||||
let outputDataItem: any;
|
||||
let printLineSpy: any;
|
||||
|
||||
this.beforeEach(() => {
|
||||
const output = rewire('../../build/framework/output');
|
||||
printLineSpy = sinon.spy();
|
||||
output.__set__('printLine', printLineSpy);
|
||||
outputDataItem = output.__get__('outputDataItem');
|
||||
});
|
||||
|
||||
it('should only output fields specified in `fields` param, in that order', async () => {
|
||||
const fields = ['id', 'name', 'thing_color'];
|
||||
const options = {};
|
||||
|
||||
await outputDataItem(dataItem, fields, options);
|
||||
|
||||
// check correct number of rows (3 fields)
|
||||
expect(printLineSpy.callCount).to.equal(3);
|
||||
// check we have fields we specified
|
||||
fields.forEach((f, index) => {
|
||||
const kvPair = printLineSpy.getCall(index).firstArg.split(':');
|
||||
expect(kvPair[0].toLowerCase()).to.include(f.replace(/_/g, ' '));
|
||||
expect(kvPair[1]).to.include((dataItem as any)[f]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only output fields specified in `options.fields` if present', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
// test all formats
|
||||
fields: 'Name,thing_color,Thing shape',
|
||||
};
|
||||
|
||||
const expectedFields = ['name', 'thing_color', 'thing_shape'];
|
||||
|
||||
await outputDataItem(dataItem, fields, options);
|
||||
|
||||
// check correct number of rows (3 fields)
|
||||
expect(printLineSpy.callCount).to.equal(3);
|
||||
// check we have fields we specified
|
||||
expectedFields.forEach((f, index) => {
|
||||
const kvPair = printLineSpy.getCall(index).firstArg.split(':');
|
||||
expect(kvPair[0].toLowerCase()).to.include(f.replace(/_/g, ' '));
|
||||
expect(kvPair[1]).to.include((dataItem as any)[f]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should output data in json format, if `options.json` true', async () => {
|
||||
const fields = ['name', 'id', 'thing_color', 'thing_shape'];
|
||||
const options = {
|
||||
json: true,
|
||||
};
|
||||
|
||||
const expectedJson = JSON.stringify(dataItem, undefined, 2);
|
||||
|
||||
await outputDataItem(dataItem, fields, options);
|
||||
|
||||
expect(printLineSpy.callCount).to.equal(1);
|
||||
expect(printLineSpy.getCall(0).firstArg).to.equal(expectedJson);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user