mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
Use standard visuals table component for fleet/s
This effectively removes the ability to filter/sort/customize the output table. The cli cannot properly handle this operations on all models and this one was inconsistent. For now we recommend that users that require parsing the CLI output use the output json format and do any kind of necessary parsing on it. Change-type: major
This commit is contained in:
parent
a08ac447a3
commit
63674c8201
@ -2168,13 +2168,9 @@ fleet name or slug (preferred)
|
||||
|
||||
open fleet dashboard page
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
## fleet pin <slug> [releaseToPinTo]
|
||||
|
||||
@ -2353,29 +2349,9 @@ Examples:
|
||||
|
||||
### Options
|
||||
|
||||
#### --fields FIELDS
|
||||
|
||||
only show provided fields (comma-separated)
|
||||
|
||||
#### -j, --json
|
||||
|
||||
output in json format
|
||||
|
||||
#### --filter FILTER
|
||||
|
||||
filter results by substring matching of a given field, eg: --filter field=foo
|
||||
|
||||
#### --no-header
|
||||
|
||||
hide table header from output
|
||||
|
||||
#### --no-truncate
|
||||
|
||||
do not truncate output to fit screen
|
||||
|
||||
#### --sort SORT
|
||||
|
||||
field to sort by (prepend '-' for descending order)
|
||||
produce JSON output instead of tabular output
|
||||
|
||||
# Local
|
||||
|
||||
|
@ -21,7 +21,6 @@ import {
|
||||
NotAvailableInOfflineModeError,
|
||||
} from './errors';
|
||||
import { stripIndent } from './utils/lazy';
|
||||
import * as output from './framework/output';
|
||||
|
||||
export default abstract class BalenaCommand extends Command {
|
||||
/**
|
||||
@ -168,7 +167,4 @@ export default abstract class BalenaCommand extends Command {
|
||||
await this.getStdin();
|
||||
}
|
||||
}
|
||||
|
||||
protected outputMessage = output.outputMessage;
|
||||
protected outputData = output.outputData;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import { Flags } from '@oclif/core';
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import * as ca from '../../utils/common-args';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
import { applicationIdInfo } from '../../utils/messages';
|
||||
|
||||
export default class FleetCmd extends Command {
|
||||
@ -49,7 +49,7 @@ export default class FleetCmd extends Command {
|
||||
default: false,
|
||||
description: 'open fleet dashboard page',
|
||||
}),
|
||||
...cf.dataOutputFlags,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -78,16 +78,28 @@ export default class FleetCmd extends Command {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputApplication = {
|
||||
...application,
|
||||
const applicationToDisplay = {
|
||||
id: application.id,
|
||||
app_name: application.app_name,
|
||||
slug: application.slug,
|
||||
device_type: application.is_for__device_type[0].slug,
|
||||
commit: application.should_be_running__release[0]?.commit,
|
||||
};
|
||||
|
||||
await this.outputData(
|
||||
outputApplication,
|
||||
['app_name', 'id', 'device_type', 'slug', 'commit'],
|
||||
options,
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(applicationToDisplay, null, 4));
|
||||
return;
|
||||
}
|
||||
|
||||
// Emulate table.vertical title output, but avoid uppercasing and inserting spaces
|
||||
console.log(`== ${applicationToDisplay.app_name}`);
|
||||
console.log(
|
||||
getVisuals().table.vertical(applicationToDisplay, [
|
||||
'id',
|
||||
'device_type',
|
||||
'slug',
|
||||
'commit',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import type * as BalenaSdk from 'balena-sdk';
|
||||
|
||||
import Command from '../../command';
|
||||
import * as cf from '../../utils/common-flags';
|
||||
import { getBalenaSdk, stripIndent } from '../../utils/lazy';
|
||||
import { getBalenaSdk, getVisuals, stripIndent } from '../../utils/lazy';
|
||||
|
||||
interface ExtendedApplication extends ApplicationWithDeviceTypeSlug {
|
||||
device_count: number;
|
||||
@ -42,8 +42,8 @@ export default class FleetsCmd extends Command {
|
||||
public static usage = 'fleets';
|
||||
|
||||
public static flags = {
|
||||
...cf.dataSetOutputFlags,
|
||||
help: cf.help,
|
||||
json: cf.json,
|
||||
};
|
||||
|
||||
public static authenticated = true;
|
||||
@ -77,17 +77,29 @@ export default class FleetsCmd extends Command {
|
||||
application.device_type = application.is_for__device_type[0].slug;
|
||||
});
|
||||
|
||||
await this.outputData(
|
||||
applications,
|
||||
[
|
||||
const applicationsToDisplay = applications.map((application) => ({
|
||||
id: application.id,
|
||||
app_name: application.app_name,
|
||||
slug: application.slug,
|
||||
device_type: application.device_type,
|
||||
online_devices: application.online_devices,
|
||||
device_count: application.device_count,
|
||||
}));
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(applicationsToDisplay, null, 4));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
getVisuals().table.horizontal(applicationsToDisplay, [
|
||||
'id',
|
||||
'app_name',
|
||||
'app_name => NAME',
|
||||
'slug',
|
||||
'device_type',
|
||||
'device_count',
|
||||
'online_devices',
|
||||
],
|
||||
options,
|
||||
'device_count',
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
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 };
|
@ -1,158 +0,0 @@
|
||||
/*
|
||||
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[] | object,
|
||||
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');
|
||||
}
|
@ -104,36 +104,3 @@ export const json = Flags.boolean({
|
||||
description: 'produce JSON output instead of tabular output',
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const dataOutputFlags = {
|
||||
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 = {
|
||||
...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)`,
|
||||
}),
|
||||
};
|
||||
|
@ -1,255 +0,0 @@
|
||||
/**
|
||||
* @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