From 949eaa7565c12117195e5b0e59b8ab846319e36a Mon Sep 17 00:00:00 2001 From: Willian Date: Wed, 26 Jun 2019 22:28:31 -0300 Subject: [PATCH 1/9] Update input widget test --- test/widgets/input.spec.ts | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/test/widgets/input.spec.ts b/test/widgets/input.spec.ts index 8096c03..ac3d470 100644 --- a/test/widgets/input.spec.ts +++ b/test/widgets/input.spec.ts @@ -1,10 +1,11 @@ import { UiElement } from 'concordialang-ui-core' -import { Input } from '../../src/widgets/input' +import { WidgetConfig } from '../../src/interfaces/app-config' +import Input from '../../src/widgets/input' describe('Input', () => { describe('renderToString', () => { - const defaultProps: UiElement = { + const uiElement: UiElement = { name: 'Username', widget: 'textbox', position: 16, @@ -16,37 +17,26 @@ describe('Input', () => { } } - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Input(uiElement.props, uiElement.name) : - new Input({}) - ) - - it('without properties', () => { - const inputWidget: Input = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) + const widgetConfig: WidgetConfig = { + opening: '', + wrapperOpening: '
', + wrapperClosure: '
' + } it('produces html from an input element with name', async () => { - const inputWidget: Input = subject(defaultProps) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) - }) - - it('produces html from an input element without name', async () => { - const inputWidget: Input = subject({ ...defaultProps, name: undefined }) + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) it('produces a label for the input element', async () => { - const inputWidget: Input = subject(defaultProps) + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringContaining('')) }) it('surrounds the input with a div', () => { - const inputWidget: Input = subject() + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) From a1469aee7052e873f24bdc9885a6f69e7b2f367b Mon Sep 17 00:00:00 2001 From: Willian Date: Sat, 29 Jun 2019 15:06:12 -0300 Subject: [PATCH 2/9] Add label definitions in app config --- dist/interfaces/app-config.d.ts | 7 +++++++ dist/utils/prop.d.ts | 1 + dist/utils/prop.js | 1 + dist/widgets/input.js | 8 ++++++-- dist/widgets/label.d.ts | 3 ++- dist/widgets/label.js | 17 ++++++++++++----- dist/widgets/widget-factory.js | 2 ++ src/interfaces/app-config.ts | 11 +++++++++-- src/utils/prop.ts | 2 ++ src/widgets/input.ts | 6 +++--- src/widgets/label.ts | 16 ++++++++++++---- src/widgets/widget-factory.ts | 1 + test/widgets/input.spec.ts | 25 ++++++++++++++++++++++--- test/widgets/widget-factory.spec.ts | 29 ++++++++++++++++++++++------- 14 files changed, 102 insertions(+), 27 deletions(-) diff --git a/dist/interfaces/app-config.d.ts b/dist/interfaces/app-config.d.ts index d56546e..533c6d2 100644 --- a/dist/interfaces/app-config.d.ts +++ b/dist/interfaces/app-config.d.ts @@ -1,6 +1,7 @@ export interface AppConfig { widgets?: { input?: WidgetConfig; + label?: LabelConfig; }; } export interface WidgetConfig { @@ -8,4 +9,10 @@ export interface WidgetConfig { closure?: string; wrapperOpening?: string; wrapperClosure?: string; + label?: LabelConfig; } +interface LabelConfig { + opening: string; + closure: string; +} +export {}; diff --git a/dist/utils/prop.d.ts b/dist/utils/prop.d.ts index c409969..9934699 100644 --- a/dist/utils/prop.d.ts +++ b/dist/utils/prop.d.ts @@ -1 +1,2 @@ +export declare const PROPS_INJECTION_POINT = "%s"; export declare function formatProperties(props: any, validProperties: string[]): string; diff --git a/dist/utils/prop.js b/dist/utils/prop.js index b4c1c4e..131cca6 100644 --- a/dist/utils/prop.js +++ b/dist/utils/prop.js @@ -1,5 +1,6 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) +exports.PROPS_INJECTION_POINT = '%s' function formatProperties(props, validProperties) { const translateProp = key => { switch (key) { diff --git a/dist/widgets/input.js b/dist/widgets/input.js index 89e7db6..901800c 100644 --- a/dist/widgets/input.js +++ b/dist/widgets/input.js @@ -23,11 +23,15 @@ class Input extends concordialang_ui_core_1.Widget { this.VALID_PROPERTIES ) const input = this._config.opening.replace( - '%s', + prop_1.PROPS_INJECTION_POINT, `${inputType} ${properties}` ) const inputClosure = this._config.closure || '' - const label = label_1.createLabel(this.name, this.props.id.toString()) + const label = label_1.createLabel( + this.name, + this.props.id.toString(), + this._config + ) return this.wrap(label + input + inputClosure) } wrap(elements) { diff --git a/dist/widgets/label.d.ts b/dist/widgets/label.d.ts index d914e3a..483a09d 100644 --- a/dist/widgets/label.d.ts +++ b/dist/widgets/label.d.ts @@ -1 +1,2 @@ -export declare function createLabel(name: string, id: string): string; +import { WidgetConfig } from '../interfaces/app-config'; +export declare function createLabel(widgetName: string, widgetId: string, widgetConfig: WidgetConfig): string; diff --git a/dist/widgets/label.js b/dist/widgets/label.js index 125f5ce..5b8099c 100644 --- a/dist/widgets/label.js +++ b/dist/widgets/label.js @@ -1,10 +1,17 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -function createLabel(name, id) { - const validIdPattern = /^(#|~|\d|\w).*/ - const labelFor = validIdPattern.test(id) - ? `for="${id.replace(/^#|~/, '')}"` +const prop_1 = require('../utils/prop') +function createLabel(widgetName, widgetId, widgetConfig) { + if (!widgetConfig.label) return '' + const idPattern = /^(#|~|\d|\w).*/ + const labelFor = widgetId.match(idPattern) + ? `for="${widgetId.replace(/^#|~/, '')}"` : '' - return `` + const labelOpening = widgetConfig.label.opening.replace( + prop_1.PROPS_INJECTION_POINT, + labelFor + ) + const labelClosure = widgetConfig.label.closure + return labelOpening + widgetName + labelClosure } exports.createLabel = createLabel diff --git a/dist/widgets/widget-factory.js b/dist/widgets/widget-factory.js index f188a64..f5a1d63 100644 --- a/dist/widgets/widget-factory.js +++ b/dist/widgets/widget-factory.js @@ -28,6 +28,8 @@ class WidgetFactory { } createInputElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.input') + widgetConfig.label = + widgetConfig.label || lodash_1.get(this._config, 'widgets.label') return new input_1.default(element.props, element.name, widgetConfig) } } diff --git a/src/interfaces/app-config.ts b/src/interfaces/app-config.ts index f52efca..59c2086 100644 --- a/src/interfaces/app-config.ts +++ b/src/interfaces/app-config.ts @@ -1,6 +1,7 @@ export interface AppConfig { widgets?: { - input?: WidgetConfig + input?: WidgetConfig, + label?: LabelConfig } } @@ -8,5 +9,11 @@ export interface WidgetConfig { opening: string, closure?: string, wrapperOpening?: string, - wrapperClosure?: string + wrapperClosure?: string, + label?: LabelConfig +} + +interface LabelConfig { + opening: string, + closure: string } diff --git a/src/utils/prop.ts b/src/utils/prop.ts index 5741272..f2ad5d1 100644 --- a/src/utils/prop.ts +++ b/src/utils/prop.ts @@ -1,3 +1,5 @@ +export const PROPS_INJECTION_POINT = '%s' + export function formatProperties(props: any, validProperties: string[]): string { const translateProp = (key: string) => { switch(key) { diff --git a/src/widgets/input.ts b/src/widgets/input.ts index 30c07f9..4c9cc09 100644 --- a/src/widgets/input.ts +++ b/src/widgets/input.ts @@ -2,7 +2,7 @@ import { Widget } from 'concordialang-ui-core' import { get } from 'lodash'; import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties } from '../utils/prop' +import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' import { createLabel } from './label' const enum DataTypes { @@ -24,9 +24,9 @@ export default class Input extends Widget { public renderToString(): string { const inputType = this.getType(this.props.datatype as string) const properties = formatProperties(this.props, this.VALID_PROPERTIES) - const input = this._config.opening.replace('%s', `${ inputType } ${ properties }`) + const input = this._config.opening.replace(PROPS_INJECTION_POINT, `${ inputType } ${ properties }`) const inputClosure = this._config.closure || '' - const label = createLabel(this.name, this.props.id.toString()) + const label = createLabel(this.name, this.props.id.toString(), this._config) return this.wrap(label + input + inputClosure) } diff --git a/src/widgets/label.ts b/src/widgets/label.ts index 51f1061..0cf8a88 100644 --- a/src/widgets/label.ts +++ b/src/widgets/label.ts @@ -1,5 +1,13 @@ -export function createLabel(name: string, id: string): string { - const validIdPattern = /^(#|~|\d|\w).*/ - const labelFor = (validIdPattern.test(id)) ? `for="${id.replace(/^#|~/ , '')}"` : '' - return `` +import { WidgetConfig } from '../interfaces/app-config' +import { PROPS_INJECTION_POINT } from '../utils/prop' + +export function createLabel(widgetName: string, widgetId: string, widgetConfig: WidgetConfig): string { + if (!widgetConfig.label) return '' + + const idPattern = /^(#|~|\d|\w).*/ + const labelFor = (widgetId.match(idPattern)) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' + const labelOpening = widgetConfig.label.opening.replace(PROPS_INJECTION_POINT, labelFor) + const labelClosure = widgetConfig.label.closure + + return labelOpening + widgetName + labelClosure } diff --git a/src/widgets/widget-factory.ts b/src/widgets/widget-factory.ts index 7bfcfa9..ad16ba5 100644 --- a/src/widgets/widget-factory.ts +++ b/src/widgets/widget-factory.ts @@ -32,6 +32,7 @@ export default class WidgetFactory { private createInputElement(element: UiElement): any { const widgetConfig: WidgetConfig = get(this._config, 'widgets.input') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') return new Input(element.props, element.name, widgetConfig) } } diff --git a/test/widgets/input.spec.ts b/test/widgets/input.spec.ts index ac3d470..833f71f 100644 --- a/test/widgets/input.spec.ts +++ b/test/widgets/input.spec.ts @@ -1,6 +1,7 @@ import { UiElement } from 'concordialang-ui-core' -import { WidgetConfig } from '../../src/interfaces/app-config' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' import Input from '../../src/widgets/input' +import { getAppConfig } from '../test-helpers/app-config' describe('Input', () => { @@ -20,7 +21,11 @@ describe('Input', () => { const widgetConfig: WidgetConfig = { opening: '', wrapperOpening: '
', - wrapperClosure: '
' + wrapperClosure: '
', + label: { + opening: '' + } } it('produces html from an input element with name', async () => { @@ -35,10 +40,24 @@ describe('Input', () => { expect(result).toEqual(expect.stringContaining('')) }) - it('surrounds the input with a div', () => { + it('produces a wrapper for the input element', () => { const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) + + describe('when the label is not defined', () => { + const widgetConfig: WidgetConfig = { + opening: '', + wrapperOpening: '
', + wrapperClosure: '
' + } + + it('does not produce a label for the input element', async () => { + const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).not.toEqual(expect.stringContaining('label')) + }) + }) }) }) diff --git a/test/widgets/widget-factory.spec.ts b/test/widgets/widget-factory.spec.ts index 9354c1a..74167ed 100644 --- a/test/widgets/widget-factory.spec.ts +++ b/test/widgets/widget-factory.spec.ts @@ -1,12 +1,25 @@ import { UiElement } from 'concordialang-ui-core' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' import WidgetFactory from '../../src/widgets/widget-factory' -import { Button } from '../../src/widgets/button'; -import { Input } from '../../src/widgets/input'; +import Button from '../../src/widgets/button'; +import Input from '../../src/widgets/input'; describe('WidgetFactory', () => { - let widgetFactory: WidgetFactory = new WidgetFactory() + const appConfig: AppConfig = { + widgets: { + input: { + opening: '', + label: { + opening: '' + } + } + } + } + + let widgetFactory: WidgetFactory = new WidgetFactory(appConfig) describe('create', () => { it('create button with valid properties', () => { @@ -18,10 +31,10 @@ describe('WidgetFactory', () => { } const buttonWidget = new Button(buttonUiElement.props, buttonUiElement.name) - + expect(widgetFactory.create(buttonUiElement)).toEqual(buttonWidget) }) - + it('create input with valid properties', async () => { const inputUiElement: UiElement = { name: 'Username', @@ -33,15 +46,17 @@ describe('WidgetFactory', () => { minlength: 10 } } + const widgetConfig: WidgetConfig = appConfig.widgets.input + + const inputWidget = new Input(inputUiElement.props, inputUiElement.name, widgetConfig) - const inputWidget = new Input(inputUiElement.props, inputUiElement.name) - expect(widgetFactory.create(inputUiElement)).toEqual(inputWidget) }) it('throw invalid widget error', async () => { const inputUiElement: UiElement = { widget: 'invalid', + name: '', position: 16, props: {} } From 9db187923fb5662fdb115d38e7c5b4740959e33a Mon Sep 17 00:00:00 2001 From: Willian Date: Mon, 1 Jul 2019 22:12:37 -0300 Subject: [PATCH 3/9] Generate all widgets based on config file --- package-lock.json | 5 +++ package.json | 1 + src/interfaces/app-config.ts | 5 +++ src/utils/prop.ts | 23 ++---------- src/widgets/button.ts | 34 +++++++++--------- src/widgets/checkbox.ts | 19 +++++----- src/widgets/input.ts | 11 ++---- src/widgets/label.ts | 2 +- src/widgets/radio.ts | 41 ++++++++++++--------- src/widgets/select.ts | 37 +++++++++++-------- src/widgets/widget-factory.ts | 33 ++++++++++++++--- src/widgets/wrapper.ts | 7 ++++ test/commands/generate.spec.ts | 7 ++-- test/generator.spec.ts | 20 ++++++----- test/utils/format-properties.spec.ts | 18 ++++++++++ test/widgets/button.spec.ts | 25 ++++++++----- test/widgets/checkbox.spec.ts | 42 ++++++++++------------ test/widgets/input.spec.ts | 2 -- test/widgets/prop.spec.ts | 2 +- test/widgets/radio.spec.ts | 48 ++++++++++++------------- test/widgets/select.spec.ts | 53 +++++++++++++++------------- 21 files changed, 248 insertions(+), 187 deletions(-) create mode 100644 src/widgets/wrapper.ts create mode 100644 test/utils/format-properties.spec.ts diff --git a/package-lock.json b/package-lock.json index b71ba09..e4ee447 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1532,6 +1532,11 @@ "redeyed": "~2.1.0" } }, + "case": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.1.tgz", + "integrity": "sha512-N0rDB5ftMDKANGsIBRWPWcG0VIKtirgqcXb2vKFi66ySAjXVEwbfCN7ass1mkdXO8fbol3RfbWlQ9KyBX2F/Gg==" + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", diff --git a/package.json b/package.json index 1b193f6..4078409 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@oclif/command": "^1.5.12", "@oclif/config": "^1.12.12", "@types/lodash": "^4.14.134", + "case": "^1.6.1", "concordialang-ui-core": "^0.2.3", "cosmiconfig": "^5.2.1", "lodash": "^4.17.11", diff --git a/src/interfaces/app-config.ts b/src/interfaces/app-config.ts index 59c2086..b462f6f 100644 --- a/src/interfaces/app-config.ts +++ b/src/interfaces/app-config.ts @@ -1,6 +1,9 @@ export interface AppConfig { widgets?: { input?: WidgetConfig, + radio?: WidgetConfig, + checkbox?: WidgetConfig, + select?: WidgetConfig, label?: LabelConfig } } @@ -8,6 +11,8 @@ export interface AppConfig { export interface WidgetConfig { opening: string, closure?: string, + optionOpening?: string, + optionClosure?: string, wrapperOpening?: string, wrapperClosure?: string, label?: LabelConfig diff --git a/src/utils/prop.ts b/src/utils/prop.ts index f2ad5d1..15d322f 100644 --- a/src/utils/prop.ts +++ b/src/utils/prop.ts @@ -1,3 +1,5 @@ +import { camel } from 'case' + export const PROPS_INJECTION_POINT = '%s' export function formatProperties(props: any, validProperties: string[]): string { @@ -9,26 +11,7 @@ export function formatProperties(props: any, validProperties: string[]): string } const getFormattedProp = (key: string) => { - let value = props[key] - const invalidIdPattern = /^\/\// - - if(key === 'id') { - let newKey = key - // TODO: replace test wit str.match(pattern) - if(!invalidIdPattern.test(value)) { - const validIdPattern = /^#|~/ - const validClassPattern = /^\./ - - if(validIdPattern.test(value)) { - value = value.toString().replace(validIdPattern, '') - } else if(validClassPattern.test(value)) { - newKey = 'class' - value = value.toString().replace(validClassPattern, '') - } - return `${translateProp(newKey)}="${value}"` - } - } - + let value = camel(props[key].toString()) return `${translateProp(key)}="${value}"` } diff --git a/src/widgets/button.ts b/src/widgets/button.ts index b036186..8a93ff9 100644 --- a/src/widgets/button.ts +++ b/src/widgets/button.ts @@ -1,22 +1,24 @@ -import {Widget} from 'concordialang-ui-core' - -import {formatProperties} from '../utils/prop' +import { Widget } from 'concordialang-ui-core' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' export default class Button extends Widget { - private readonly VALID_PROPERTIES = ['id', 'disabled', 'value'] + private readonly VALID_PROPERTIES = ['id', 'disabled', 'value'] - constructor(props: any, name?: string) { - super(props, name || '') - } + constructor(props: any, name: string, private _config: WidgetConfig) { + super(props, name || '') + } - public renderToString(): string { - // const inputType = this.getType(this.props.datatype as string) - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - // return `` - return `` - } + public renderToString(): string { + const buttonType = this.getType(this.props.datatype as string) + let properties = formatProperties(this.props, this.VALID_PROPERTIES) + properties = `${ buttonType } ${ properties }` + const buttonOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) + const buttonClosure = this._config.closure + return buttonOpening + this.name + buttonClosure + } - private getType(datatype: string): string { - return `type="${datatype || 'button'}"` - } + private getType(datatype: string): string { + return `type="${datatype || 'button'}"` + } } diff --git a/src/widgets/checkbox.ts b/src/widgets/checkbox.ts index 5e37c09..4c78bca 100644 --- a/src/widgets/checkbox.ts +++ b/src/widgets/checkbox.ts @@ -1,18 +1,21 @@ -import {Widget} from 'concordialang-ui-core' - -import {formatProperties} from '../utils/prop' +import { Widget } from 'concordialang-ui-core' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' +import { wrap } from './wrapper' export default class Checkbox extends Widget { private readonly VALID_PROPERTIES = ['value', 'required'] - constructor(props: any, name: string) { + constructor(props: any, name: string, private _config: WidgetConfig) { super(props, name) } - // TODO: remove \n public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - if (properties) return `
\n${this.name}\n
` - return `
\n${this.name}\n
` + const inputType = 'type="checkbox"' + let properties = formatProperties(this.props, this.VALID_PROPERTIES) + properties = `${inputType} ${properties}` + const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) + const inputClosure = this._config.closure || '' + return wrap(inputOpening + this.name + inputClosure, this._config) } } diff --git a/src/widgets/input.ts b/src/widgets/input.ts index 4c9cc09..adb938e 100644 --- a/src/widgets/input.ts +++ b/src/widgets/input.ts @@ -4,6 +4,7 @@ import { get } from 'lodash'; import { WidgetConfig } from '../interfaces/app-config' import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' import { createLabel } from './label' +import { wrap } from './wrapper' const enum DataTypes { STRING = 'string', @@ -24,16 +25,10 @@ export default class Input extends Widget { public renderToString(): string { const inputType = this.getType(this.props.datatype as string) const properties = formatProperties(this.props, this.VALID_PROPERTIES) - const input = this._config.opening.replace(PROPS_INJECTION_POINT, `${ inputType } ${ properties }`) + const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, `${ inputType } ${ properties }`) const inputClosure = this._config.closure || '' const label = createLabel(this.name, this.props.id.toString(), this._config) - return this.wrap(label + input + inputClosure) - } - - private wrap(elements: string): string { - if (this._config.wrapperOpening && this._config.wrapperClosure) - return this._config.wrapperOpening + elements + this._config.wrapperClosure - return elements + return wrap(label + inputOpening + inputClosure, this._config) } private getType(datatype: string): string { diff --git a/src/widgets/label.ts b/src/widgets/label.ts index 0cf8a88..a75a5e2 100644 --- a/src/widgets/label.ts +++ b/src/widgets/label.ts @@ -5,7 +5,7 @@ export function createLabel(widgetName: string, widgetId: string, widgetConfig: if (!widgetConfig.label) return '' const idPattern = /^(#|~|\d|\w).*/ - const labelFor = (widgetId.match(idPattern)) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' + const labelFor = widgetId.match(idPattern) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' const labelOpening = widgetConfig.label.opening.replace(PROPS_INJECTION_POINT, labelFor) const labelClosure = widgetConfig.label.closure diff --git a/src/widgets/radio.ts b/src/widgets/radio.ts index 60f104d..0044ec3 100644 --- a/src/widgets/radio.ts +++ b/src/widgets/radio.ts @@ -1,30 +1,37 @@ -import {Widget} from 'concordialang-ui-core' - -import {formatProperties} from '../utils/prop' -import {createLabel} from './label' +import { Widget } from 'concordialang-ui-core' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' +import { createLabel } from './label' +import { wrap } from './wrapper' export default class Radio extends Widget { - private readonly VALID_PROPERTIES = ['value'] + private readonly VALID_PROPERTIES = [ 'value' ] - constructor(props: any, name: string) { + constructor(props: any, name: string, private _config: WidgetConfig) { super(props, name) } - // TODO: remove \n public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) + const inputType = 'type="radio"' + const label = createLabel(this.name, '', this._config) let inputs: String[] = [] - const label = createLabel(this.name, this.props.id.toString()) - const inputName = this.name.toLowerCase() - if (properties) { - for (let value of this.props.value as Array) { - let input = `${value}` - inputs.push(input) - } - return `
\n${label + inputs.join('\n')}\n
` + for (let value of this.props.value as Array) { + // TODO: o que fazer no formatProperties em relação ao value? + // provavelmente terei que instalar o pacote "case" + // para ter 'value="algumaCoisa"', quando value for "Alguma Coisa" + // + // TODO: adicionar propriedades 'id' e 'nome' + + const props = Object.assign({}, this.props, { value }) + let properties = formatProperties(props, this.VALID_PROPERTIES) + properties = `${inputType} ${properties}` + const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) + const inputClosure = this._config.closure || '' + inputs.push(inputOpening + value + inputClosure) } - return '
\n\n
' + + return wrap(label + inputs.join(''), this._config) } } diff --git a/src/widgets/select.ts b/src/widgets/select.ts index 1994f1e..856a86f 100644 --- a/src/widgets/select.ts +++ b/src/widgets/select.ts @@ -1,31 +1,38 @@ -import {Widget} from 'concordialang-ui-core' - -import {formatProperties} from '../utils/prop' -import {createLabel} from './label' +import { Widget } from 'concordialang-ui-core' +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' +import { createLabel } from './label' +import { wrap } from './wrapper' export default class Select extends Widget { - private readonly VALID_PROPERTIES = ['id', 'required'] + private readonly SELECT_VALID_PROPERTIES = ['id', 'required'] + private readonly OPTION_VALID_PROPERTIES = ['value'] - constructor(props: any, name: string) { + constructor(props: any, name: string, private _config: WidgetConfig) { super(props, name) } - // TODO: remove \n public renderToString(): string { - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - if (!properties) return '
\n\n
' + const properties = formatProperties(this.props, this.SELECT_VALID_PROPERTIES) + const selectOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) + const selectClosure = this._config.closure const options = this.getOptions() - const select = `\n` - const label = createLabel(this.name, this.props.id.toString()) - return `
\n${label + select}
` + const select = selectOpening + options + selectClosure + const label = createLabel(this.name, this.props.id.toString(), this._config) + return wrap(label + select, this._config) } private getOptions(): string { + if (!this._config.optionOpening) return '' + let options: string[] = [] for (let value of this.props.value as Array) { - let option = `` - options.push(option) + const optionProps = { value } + const properties = formatProperties(optionProps, this.OPTION_VALID_PROPERTIES) + const optionOpening = this._config.optionOpening.replace(PROPS_INJECTION_POINT, properties) + const optionClosure = this._config.optionClosure + options.push(optionOpening + value + optionClosure) } - return options.join('\n') + return options.join('') } } diff --git a/src/widgets/widget-factory.ts b/src/widgets/widget-factory.ts index ad16ba5..e6dca4c 100644 --- a/src/widgets/widget-factory.ts +++ b/src/widgets/widget-factory.ts @@ -22,17 +22,40 @@ export default class WidgetFactory { create(element: UiElement): Widget { switch (element.widget) { case Widgets.TEXTBOX: return this.createInputElement(element) - case Widgets.BUTTON: return new Button(element.props, element.name) - case Widgets.CHECKBOX: return new Checkbox(element.props, element.name) - case Widgets.RADIO: return new Radio(element.props, element.name) - case Widgets.SELECT: return new Select(element.props, element.name) + case Widgets.BUTTON: return this.createButtonElement(element) + case Widgets.CHECKBOX: return this.createCheckboxElement(element) + case Widgets.RADIO: return this.createRadioElement(element) + case Widgets.SELECT: return this.createSelectElement(element) default: throw new Error(`Invalid widget type: ${element.widget}`) } } - private createInputElement(element: UiElement): any { + private createInputElement(element: UiElement): Input { const widgetConfig: WidgetConfig = get(this._config, 'widgets.input') widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') return new Input(element.props, element.name, widgetConfig) } + + private createRadioElement(element: UiElement): Radio { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.radio') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + return new Radio(element.props, element.name, widgetConfig) + } + + private createCheckboxElement(element: UiElement): Checkbox { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.checkbox') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + return new Checkbox(element.props, element.name, widgetConfig) + } + + private createSelectElement(element: UiElement): Select { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.select') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + return new Select(element.props, element.name, widgetConfig) + } + + private createButtonElement(element: UiElement): Button { + const widgetConfig: WidgetConfig = get(this._config, 'widgets.button') + return new Button(element.props, element.name, widgetConfig) + } } diff --git a/src/widgets/wrapper.ts b/src/widgets/wrapper.ts new file mode 100644 index 0000000..5925db1 --- /dev/null +++ b/src/widgets/wrapper.ts @@ -0,0 +1,7 @@ +import { WidgetConfig } from '../interfaces/app-config' + +export function wrap(elements: string, widgetConfig: WidgetConfig): string { + if (widgetConfig.wrapperOpening && widgetConfig.wrapperClosure) + return widgetConfig.wrapperOpening + elements + widgetConfig.wrapperClosure + return elements +} diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index fd7d496..f6e04f0 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -4,9 +4,8 @@ describe('Generate', () => { it('should print a JSON content', async () => { let spy = jest.spyOn(process.stdout, 'write'); - await Generate.run([]) // TODO: pass a parameter - // expect(spy).toBeCalledWith({}) - expect(spy).not.toBeCalled() // TODO: change this assertion + await Generate.run(['--features', '[]', '--outputDir', 'outputDir']) + expect(spy).not.toBeCalled() }) -}) \ No newline at end of file +}) diff --git a/test/generator.spec.ts b/test/generator.spec.ts index d574429..b917c69 100644 --- a/test/generator.spec.ts +++ b/test/generator.spec.ts @@ -3,29 +3,31 @@ import { minify } from 'html-minifier' import { fs, vol } from 'memfs' import { promisify } from 'util' -import Generator from '../src/generator' +import HtmlUIPrototyper from '../src/html-ui-prototyper' -describe('Generator', () => { +describe('HtmlUIPrototyper', () => { const CURRENT_DIR: string = process.cwd() - let generator: Generator | null + let prototyper: HtmlUIPrototyper | null beforeEach(() => { - vol.mkdirpSync(CURRENT_DIR) // Synchronize with the current fs structure - generator = new Generator(fs) // In-memory fs + vol.fromJSON({ + '/concordialang-ui-html.json': '{}' + }, CURRENT_DIR) + prototyper = new HtmlUIPrototyper(fs, CURRENT_DIR) // In-memory fs }) afterEach(() => { vol.reset() // Erase in-memory structure - generator = null + prototyper = null }) async function expectFeaturesToProduceHtml(features: Feature[], htmls: string[]): Promise { - if (! generator) { - generator = new Generator(fs) + if (! prototyper) { + prototyper = new HtmlUIPrototyper(fs, CURRENT_DIR) } - const files: string[] = await generator.generate(features) + const files: string[] = await prototyper.generate(features) expect(files).toHaveLength(htmls.length) // tslint:disable-next-line:forin for (let i in files) { diff --git a/test/utils/format-properties.spec.ts b/test/utils/format-properties.spec.ts new file mode 100644 index 0000000..17bc7ae --- /dev/null +++ b/test/utils/format-properties.spec.ts @@ -0,0 +1,18 @@ +import { formatProperties } from '../../src/utils/prop' + +describe('formatProperties', () => { + describe('when there is an invalid property', () => { + const props = { + id: 'id', + name: 'name', + required: true, + foo: 'bar' + } + + const validProperties = ['id', 'name', 'required'] + + it('produces a string with the valid properties only', () => { + expect(formatProperties(props, validProperties)).toEqual('id="id" name="name" required="true"') + }) + }) +}) diff --git a/test/widgets/button.spec.ts b/test/widgets/button.spec.ts index d3c0d99..c46a90f 100644 --- a/test/widgets/button.spec.ts +++ b/test/widgets/button.spec.ts @@ -1,20 +1,29 @@ import { UiElement} from 'concordialang-ui-core' -import { Button } from '../../src/widgets/button' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' +import Button from '../../src/widgets/button' describe('Button', () => { describe('renderToString', () => { - it('without properties', () => { - const b = new Button({}) - expect(b.renderToString()).toBe('') - }) + const uiElement: UiElement = { + name: 'Save', + widget: 'button', + position: 7, + props: { + id: 'save' + } + } + + const widgetConfig: WidgetConfig = { + opening: '' + } it('produces html from a button element', async () => { - const buttonUiElement: UiElement = { name: 'OK', widget: 'button', position: 30, props: {} } - const buttonWidget: Button = new Button(buttonUiElement.props, buttonUiElement.name) + const buttonWidget: Button = new Button(uiElement.props, uiElement.name, widgetConfig) const result = buttonWidget.renderToString() - expect(result).toEqual(``) + expect(result).toEqual(``) }) }) }) diff --git a/test/widgets/checkbox.spec.ts b/test/widgets/checkbox.spec.ts index 84e1b1b..5bea6c6 100644 --- a/test/widgets/checkbox.spec.ts +++ b/test/widgets/checkbox.spec.ts @@ -1,10 +1,11 @@ import { UiElement } from 'concordialang-ui-core' -import { Checkbox } from '../../src/widgets/checkbox' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' +import Checkbox from '../../src/widgets/checkbox' describe('Checkbox', () => { describe('renderToString', () => { - const defaultProps: UiElement = { + const uiElement: UiElement = { name: 'Web Developer', widget: 'checkbox', position: 16, @@ -13,33 +14,26 @@ describe('Checkbox', () => { } } - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Checkbox(uiElement.props, uiElement.name) : - new Checkbox({}) - ) + const widgetConfig: WidgetConfig = { + opening: '', + wrapperOpening: '
', + wrapperClosure: '
', + label: { + opening: '' + } + } - it('without properties', () => { - const inputWidget: Checkbox = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) + it('produces html from an input element with name', async () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('Web Developer')) }) - it('surrounds the input with a div', () => { - const inputWidget: Checkbox = subject() + it('produces a wrapper for the input element', () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) - - it('produces html from an input element with name', async () => { - const inputWidget: Checkbox = subject(defaultProps) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Web Developer')) - }) - - it('produces html from an input element without name', async () => { - const inputWidget: Checkbox = subject({ ...defaultProps, name: undefined }) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching('\n')) - }) }) }) diff --git a/test/widgets/input.spec.ts b/test/widgets/input.spec.ts index 833f71f..c68fb18 100644 --- a/test/widgets/input.spec.ts +++ b/test/widgets/input.spec.ts @@ -1,10 +1,8 @@ import { UiElement } from 'concordialang-ui-core' import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' import Input from '../../src/widgets/input' -import { getAppConfig } from '../test-helpers/app-config' describe('Input', () => { - describe('renderToString', () => { const uiElement: UiElement = { name: 'Username', diff --git a/test/widgets/prop.spec.ts b/test/widgets/prop.spec.ts index fccdacd..d0e748a 100644 --- a/test/widgets/prop.spec.ts +++ b/test/widgets/prop.spec.ts @@ -1,4 +1,4 @@ -import { formatProperties } from '../../src/widgets/prop' +import { formatProperties } from '../../src/utils/prop' describe('formatProperties', () => { it('creates a string with the valid properties', () => { diff --git a/test/widgets/radio.spec.ts b/test/widgets/radio.spec.ts index fa9286e..1e5c73d 100644 --- a/test/widgets/radio.spec.ts +++ b/test/widgets/radio.spec.ts @@ -1,9 +1,10 @@ import { UiElement } from 'concordialang-ui-core' -import { Radio } from '../../src/widgets/radio' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' +import Radio from '../../src/widgets/radio' describe('Radio', () => { describe('renderToString', () => { - const defaultProps: UiElement = { + const uiElement: UiElement = { name: 'Gender', widget: 'radio', position: 7, @@ -13,34 +14,33 @@ describe('Radio', () => { } } - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Radio(uiElement.props, uiElement.name) : - new Radio({}) - ) + const widgetConfig: WidgetConfig = { + opening: '', + wrapperOpening: '
', + wrapperClosure: '
', + label: { + opening: '' + } + } - it('without properties', () => { - const inputWidget: Radio = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) + it('produces html from an radio element with name', async () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('Male')) + expect(result).toEqual(expect.stringContaining('Female')) }) - it('surrounds the input with a div', () => { - const inputWidget: Radio = subject() - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) - }) - - it('produces html from an input element with name', async () => { - const inputWidget: Radio = subject(defaultProps) + it('produces a label for the input element', async () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Male')) - expect(result).toEqual(expect.stringContaining('Female')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) - it('produces a label for the select element', async () => { - const inputWidget: Radio = subject(defaultProps) + it('produces a wrapper for the input element', () => { + const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) }) }) diff --git a/test/widgets/select.spec.ts b/test/widgets/select.spec.ts index 637656c..eca3546 100644 --- a/test/widgets/select.spec.ts +++ b/test/widgets/select.spec.ts @@ -1,9 +1,10 @@ import { UiElement } from 'concordialang-ui-core' -import { Select } from '../../src/widgets/select' +import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' +import Select from '../../src/widgets/select' describe('Select', () => { describe('renderToString', () => { - const defaultProps: UiElement = { + const uiElement: UiElement = { name: 'Gender', widget: 'select', position: 7, @@ -13,41 +14,43 @@ describe('Select', () => { } } - const subject = (uiElement?: UiElement) => ( - uiElement ? - new Select(uiElement.props, uiElement.name) : - new Select({}) - ) - - it('without properties', () => { - const inputWidget: Select = subject() - expect(inputWidget.renderToString()).toEqual(expect.stringContaining('')) - }) - - it('surrounds the select with a div', () => { - const inputWidget: Select = subject() - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) - }) + const widgetConfig: WidgetConfig = { + opening: '', + optionOpening: '', + wrapperOpening: '
', + wrapperClosure: '
', + label: { + opening: '' + } + } it('produces html from an select element with name', async () => { - const inputWidget: Select = subject(defaultProps) + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringContaining('')) }) + it('produces the options for the select element', async () => { + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) + }) + it('produces a label for the select element', async () => { - const inputWidget: Select = subject(defaultProps) + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() expect(result).toEqual(expect.stringContaining('')) }) - it('produces the options for the select element', async () => { - const inputWidget: Select = subject(defaultProps) - const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) - expect(result).toEqual(expect.stringContaining('')) + it('produces a wrapper for the input element', () => { + const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) }) }) From b6feccce2a26967dce2f6480d1bdc84a4aa941f7 Mon Sep 17 00:00:00 2001 From: Willian Date: Mon, 1 Jul 2019 22:13:08 -0300 Subject: [PATCH 4/9] Update dist files --- dist/interfaces/app-config.d.ts | 5 ++++ dist/utils/prop.js | 19 ++------------- dist/widgets/button.d.ts | 4 ++- dist/widgets/button.js | 16 ++++++++---- dist/widgets/checkbox.d.ts | 4 ++- dist/widgets/checkbox.js | 23 +++++++++++------ dist/widgets/input.d.ts | 1 - dist/widgets/input.js | 14 +++-------- dist/widgets/radio.d.ts | 4 ++- dist/widgets/radio.js | 39 +++++++++++++++++------------ dist/widgets/select.d.ts | 7 ++++-- dist/widgets/select.js | 42 +++++++++++++++++++++++--------- dist/widgets/widget-factory.d.ts | 4 +++ dist/widgets/widget-factory.js | 30 ++++++++++++++++++++--- dist/widgets/wrapper.d.ts | 2 ++ dist/widgets/wrapper.js | 10 ++++++++ 16 files changed, 147 insertions(+), 77 deletions(-) create mode 100644 dist/widgets/wrapper.d.ts create mode 100644 dist/widgets/wrapper.js diff --git a/dist/interfaces/app-config.d.ts b/dist/interfaces/app-config.d.ts index 533c6d2..9f58ef3 100644 --- a/dist/interfaces/app-config.d.ts +++ b/dist/interfaces/app-config.d.ts @@ -1,12 +1,17 @@ export interface AppConfig { widgets?: { input?: WidgetConfig; + radio?: WidgetConfig; + checkbox?: WidgetConfig; + select?: WidgetConfig; label?: LabelConfig; }; } export interface WidgetConfig { opening: string; closure?: string; + optionOpening?: string; + optionClosure?: string; wrapperOpening?: string; wrapperClosure?: string; label?: LabelConfig; diff --git a/dist/utils/prop.js b/dist/utils/prop.js index 131cca6..b4aede4 100644 --- a/dist/utils/prop.js +++ b/dist/utils/prop.js @@ -1,5 +1,6 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) +const case_1 = require('case') exports.PROPS_INJECTION_POINT = '%s' function formatProperties(props, validProperties) { const translateProp = key => { @@ -11,23 +12,7 @@ function formatProperties(props, validProperties) { } } const getFormattedProp = key => { - let value = props[key] - const invalidIdPattern = /^\/\// - if (key === 'id') { - let newKey = key - // TODO: replace test wit str.match(pattern) - if (!invalidIdPattern.test(value)) { - const validIdPattern = /^#|~/ - const validClassPattern = /^\./ - if (validIdPattern.test(value)) { - value = value.toString().replace(validIdPattern, '') - } else if (validClassPattern.test(value)) { - newKey = 'class' - value = value.toString().replace(validClassPattern, '') - } - return `${translateProp(newKey)}="${value}"` - } - } + let value = case_1.camel(props[key].toString()) return `${translateProp(key)}="${value}"` } const formatValid = (result, prop) => { diff --git a/dist/widgets/button.d.ts b/dist/widgets/button.d.ts index 906d5f2..22f036a 100644 --- a/dist/widgets/button.d.ts +++ b/dist/widgets/button.d.ts @@ -1,7 +1,9 @@ import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; export default class Button extends Widget { + private _config; private readonly VALID_PROPERTIES; - constructor(props: any, name?: string); + constructor(props: any, name: string, _config: WidgetConfig); renderToString(): string; private getType; } diff --git a/dist/widgets/button.js b/dist/widgets/button.js index 3c35e32..d77d82d 100644 --- a/dist/widgets/button.js +++ b/dist/widgets/button.js @@ -3,18 +3,24 @@ Object.defineProperty(exports, '__esModule', { value: true }) const concordialang_ui_core_1 = require('concordialang-ui-core') const prop_1 = require('../utils/prop') class Button extends concordialang_ui_core_1.Widget { - constructor(props, name) { + constructor(props, name, _config) { super(props, name || '') + this._config = _config this.VALID_PROPERTIES = ['id', 'disabled', 'value'] } renderToString() { - // const inputType = this.getType(this.props.datatype as string) - const properties = prop_1.formatProperties( + const buttonType = this.getType(this.props.datatype) + let properties = prop_1.formatProperties( this.props, this.VALID_PROPERTIES ) - // return `` - return `` + properties = `${buttonType} ${properties}` + const buttonOpening = this._config.opening.replace( + prop_1.PROPS_INJECTION_POINT, + properties + ) + const buttonClosure = this._config.closure + return buttonOpening + this.name + buttonClosure } getType(datatype) { return `type="${datatype || 'button'}"` diff --git a/dist/widgets/checkbox.d.ts b/dist/widgets/checkbox.d.ts index b7204c6..8128427 100644 --- a/dist/widgets/checkbox.d.ts +++ b/dist/widgets/checkbox.d.ts @@ -1,6 +1,8 @@ import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; export default class Checkbox extends Widget { + private _config; private readonly VALID_PROPERTIES; - constructor(props: any, name: string); + constructor(props: any, name: string, _config: WidgetConfig); renderToString(): string; } diff --git a/dist/widgets/checkbox.js b/dist/widgets/checkbox.js index f322947..4762708 100644 --- a/dist/widgets/checkbox.js +++ b/dist/widgets/checkbox.js @@ -2,22 +2,29 @@ Object.defineProperty(exports, '__esModule', { value: true }) const concordialang_ui_core_1 = require('concordialang-ui-core') const prop_1 = require('../utils/prop') +const wrapper_1 = require('./wrapper') class Checkbox extends concordialang_ui_core_1.Widget { - constructor(props, name) { + constructor(props, name, _config) { super(props, name) + this._config = _config this.VALID_PROPERTIES = ['value', 'required'] } - // TODO: remove \n renderToString() { - const properties = prop_1.formatProperties( + const inputType = 'type="checkbox"' + let properties = prop_1.formatProperties( this.props, this.VALID_PROPERTIES ) - if (properties) - return `
\n${ - this.name - }\n
` - return `
\n${this.name}\n
` + properties = `${inputType} ${properties}` + const inputOpening = this._config.opening.replace( + prop_1.PROPS_INJECTION_POINT, + properties + ) + const inputClosure = this._config.closure || '' + return wrapper_1.wrap( + inputOpening + this.name + inputClosure, + this._config + ) } } exports.default = Checkbox diff --git a/dist/widgets/input.d.ts b/dist/widgets/input.d.ts index eb72d3a..e95736b 100644 --- a/dist/widgets/input.d.ts +++ b/dist/widgets/input.d.ts @@ -5,6 +5,5 @@ export default class Input extends Widget { private readonly VALID_PROPERTIES; constructor(props: any, name: string, _config: WidgetConfig); renderToString(): string; - private wrap; private getType; } diff --git a/dist/widgets/input.js b/dist/widgets/input.js index 901800c..3a81976 100644 --- a/dist/widgets/input.js +++ b/dist/widgets/input.js @@ -3,6 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true }) const concordialang_ui_core_1 = require('concordialang-ui-core') const prop_1 = require('../utils/prop') const label_1 = require('./label') +const wrapper_1 = require('./wrapper') class Input extends concordialang_ui_core_1.Widget { constructor(props, name, _config) { super(props, name) @@ -22,7 +23,7 @@ class Input extends concordialang_ui_core_1.Widget { this.props, this.VALID_PROPERTIES ) - const input = this._config.opening.replace( + const inputOpening = this._config.opening.replace( prop_1.PROPS_INJECTION_POINT, `${inputType} ${properties}` ) @@ -32,16 +33,7 @@ class Input extends concordialang_ui_core_1.Widget { this.props.id.toString(), this._config ) - return this.wrap(label + input + inputClosure) - } - wrap(elements) { - if (this._config.wrapperOpening && this._config.wrapperClosure) - return ( - this._config.wrapperOpening + - elements + - this._config.wrapperClosure - ) - return elements + return wrapper_1.wrap(label + inputOpening + inputClosure, this._config) } getType(datatype) { let typeProperty diff --git a/dist/widgets/radio.d.ts b/dist/widgets/radio.d.ts index 594884f..e7a0a80 100644 --- a/dist/widgets/radio.d.ts +++ b/dist/widgets/radio.d.ts @@ -1,6 +1,8 @@ import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; export default class Radio extends Widget { + private _config; private readonly VALID_PROPERTIES; - constructor(props: any, name: string); + constructor(props: any, name: string, _config: WidgetConfig); renderToString(): string; } diff --git a/dist/widgets/radio.js b/dist/widgets/radio.js index c1c3c1d..c3b4012 100644 --- a/dist/widgets/radio.js +++ b/dist/widgets/radio.js @@ -3,28 +3,37 @@ Object.defineProperty(exports, '__esModule', { value: true }) const concordialang_ui_core_1 = require('concordialang-ui-core') const prop_1 = require('../utils/prop') const label_1 = require('./label') +const wrapper_1 = require('./wrapper') class Radio extends concordialang_ui_core_1.Widget { - constructor(props, name) { + constructor(props, name, _config) { super(props, name) + this._config = _config this.VALID_PROPERTIES = ['value'] } - // TODO: remove \n renderToString() { - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) + const inputType = 'type="radio"' + const label = label_1.createLabel(this.name, '', this._config) let inputs = [] - const label = label_1.createLabel(this.name, this.props.id.toString()) - const inputName = this.name.toLowerCase() - if (properties) { - for (let value of this.props.value) { - let input = `${value}` - inputs.push(input) - } - return `
\n${label + inputs.join('\n')}\n
` + for (let value of this.props.value) { + // TODO: o que fazer no formatProperties em relação ao value? + // provavelmente terei que instalar o pacote "case" + // para ter 'value="algumaCoisa"', quando value for "Alguma Coisa" + // + // TODO: adicionar propriedades 'id' e 'nome' + const props = Object.assign({}, this.props, { value }) + let properties = prop_1.formatProperties( + props, + this.VALID_PROPERTIES + ) + properties = `${inputType} ${properties}` + const inputOpening = this._config.opening.replace( + prop_1.PROPS_INJECTION_POINT, + properties + ) + const inputClosure = this._config.closure || '' + inputs.push(inputOpening + value + inputClosure) } - return '
\n\n
' + return wrapper_1.wrap(label + inputs.join(''), this._config) } } exports.default = Radio diff --git a/dist/widgets/select.d.ts b/dist/widgets/select.d.ts index 463e827..1a81fc7 100644 --- a/dist/widgets/select.d.ts +++ b/dist/widgets/select.d.ts @@ -1,7 +1,10 @@ import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; export default class Select extends Widget { - private readonly VALID_PROPERTIES; - constructor(props: any, name: string); + private _config; + private readonly SELECT_VALID_PROPERTIES; + private readonly OPTION_VALID_PROPERTIES; + constructor(props: any, name: string, _config: WidgetConfig); renderToString(): string; private getOptions; } diff --git a/dist/widgets/select.js b/dist/widgets/select.js index fc0979c..f748755 100644 --- a/dist/widgets/select.js +++ b/dist/widgets/select.js @@ -3,30 +3,50 @@ Object.defineProperty(exports, '__esModule', { value: true }) const concordialang_ui_core_1 = require('concordialang-ui-core') const prop_1 = require('../utils/prop') const label_1 = require('./label') +const wrapper_1 = require('./wrapper') class Select extends concordialang_ui_core_1.Widget { - constructor(props, name) { + constructor(props, name, _config) { super(props, name) - this.VALID_PROPERTIES = ['id', 'required'] + this._config = _config + this.SELECT_VALID_PROPERTIES = ['id', 'required'] + this.OPTION_VALID_PROPERTIES = ['value'] } - // TODO: remove \n renderToString() { const properties = prop_1.formatProperties( this.props, - this.VALID_PROPERTIES + this.SELECT_VALID_PROPERTIES ) - if (!properties) return '
\n\n
' + const selectOpening = this._config.opening.replace( + prop_1.PROPS_INJECTION_POINT, + properties + ) + const selectClosure = this._config.closure const options = this.getOptions() - const select = `\n` - const label = label_1.createLabel(this.name, this.props.id.toString()) - return `
\n${label + select}
` + const select = selectOpening + options + selectClosure + const label = label_1.createLabel( + this.name, + this.props.id.toString(), + this._config + ) + return wrapper_1.wrap(label + select, this._config) } getOptions() { + if (!this._config.optionOpening) return '' let options = [] for (let value of this.props.value) { - let option = `` - options.push(option) + const optionProps = { value } + const properties = prop_1.formatProperties( + optionProps, + this.OPTION_VALID_PROPERTIES + ) + const optionOpening = this._config.optionOpening.replace( + prop_1.PROPS_INJECTION_POINT, + properties + ) + const optionClosure = this._config.optionClosure + options.push(optionOpening + value + optionClosure) } - return options.join('\n') + return options.join('') } } exports.default = Select diff --git a/dist/widgets/widget-factory.d.ts b/dist/widgets/widget-factory.d.ts index 5cd426e..417a0cf 100644 --- a/dist/widgets/widget-factory.d.ts +++ b/dist/widgets/widget-factory.d.ts @@ -5,4 +5,8 @@ export default class WidgetFactory { constructor(_config: AppConfig); create(element: UiElement): Widget; private createInputElement; + private createRadioElement; + private createCheckboxElement; + private createSelectElement; + private createButtonElement; } diff --git a/dist/widgets/widget-factory.js b/dist/widgets/widget-factory.js index f5a1d63..c5dfd23 100644 --- a/dist/widgets/widget-factory.js +++ b/dist/widgets/widget-factory.js @@ -15,13 +15,13 @@ class WidgetFactory { case 'textbox' /* TEXTBOX */: return this.createInputElement(element) case 'button' /* BUTTON */: - return new button_1.default(element.props, element.name) + return this.createButtonElement(element) case 'checkbox' /* CHECKBOX */: - return new checkbox_1.default(element.props, element.name) + return this.createCheckboxElement(element) case 'radio' /* RADIO */: - return new radio_1.default(element.props, element.name) + return this.createRadioElement(element) case 'select' /* SELECT */: - return new select_1.default(element.props, element.name) + return this.createSelectElement(element) default: throw new Error(`Invalid widget type: ${element.widget}`) } @@ -32,5 +32,27 @@ class WidgetFactory { widgetConfig.label || lodash_1.get(this._config, 'widgets.label') return new input_1.default(element.props, element.name, widgetConfig) } + createRadioElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.radio') + widgetConfig.label = + widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + return new radio_1.default(element.props, element.name, widgetConfig) + } + createCheckboxElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.checkbox') + widgetConfig.label = + widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + return new checkbox_1.default(element.props, element.name, widgetConfig) + } + createSelectElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.select') + widgetConfig.label = + widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + return new select_1.default(element.props, element.name, widgetConfig) + } + createButtonElement(element) { + const widgetConfig = lodash_1.get(this._config, 'widgets.button') + return new button_1.default(element.props, element.name, widgetConfig) + } } exports.default = WidgetFactory diff --git a/dist/widgets/wrapper.d.ts b/dist/widgets/wrapper.d.ts new file mode 100644 index 0000000..63e9a5b --- /dev/null +++ b/dist/widgets/wrapper.d.ts @@ -0,0 +1,2 @@ +import { WidgetConfig } from '../interfaces/app-config'; +export declare function wrap(elements: string, widgetConfig: WidgetConfig): string; diff --git a/dist/widgets/wrapper.js b/dist/widgets/wrapper.js new file mode 100644 index 0000000..45fa851 --- /dev/null +++ b/dist/widgets/wrapper.js @@ -0,0 +1,10 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +function wrap(elements, widgetConfig) { + if (widgetConfig.wrapperOpening && widgetConfig.wrapperClosure) + return ( + widgetConfig.wrapperOpening + elements + widgetConfig.wrapperClosure + ) + return elements +} +exports.wrap = wrap From dd066dcdcb6d29c499284922c5c205e40072ff6a Mon Sep 17 00:00:00 2001 From: Willian Date: Tue, 2 Jul 2019 01:23:54 -0300 Subject: [PATCH 5/9] Fix tests --- src/commands/generate.ts | 2 +- src/html-ui-prototyper.ts | 5 +++- test/commands/generate.spec.ts | 24 +++++++++++++++-- ...tor.spec.ts => html-ui-prototyper.spec.ts} | 26 ++++++++++++++++--- 4 files changed, 49 insertions(+), 8 deletions(-) rename test/{generator.spec.ts => html-ui-prototyper.spec.ts} (67%) diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 1f59803..51468d9 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -21,7 +21,7 @@ export default class Generate extends Command { const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult const generator = new HtmlUIPrototyper(fs, flags.outputDir) const result = await generator.generate(processResult.features) - this.log(JSON.stringify(result)) + this.log(result.join('\n')) } } } diff --git a/src/html-ui-prototyper.ts b/src/html-ui-prototyper.ts index b4a091a..77f443a 100644 --- a/src/html-ui-prototyper.ts +++ b/src/html-ui-prototyper.ts @@ -15,6 +15,9 @@ export default class HtmlUIPrototyper implements Prototyper { public async generate(features: Feature[]): Promise { const appConfig: AppConfig = this.getAppConfig() const factory = new WidgetFactory(appConfig) + + if (features.length === 0) return Promise.resolve([ 'No features found' ]) + const createFilePromises: Promise[] = [] for (let feature of features) { @@ -30,7 +33,7 @@ export default class HtmlUIPrototyper implements Prototyper { return result + widget.renderToString() }, '') - content = prettier.format(`
\n${content}
`, {parser: 'html', htmlWhitespaceSensitivity: 'ignore'}) + content = prettier.format(`
${content}
`, {parser: 'html', htmlWhitespaceSensitivity: 'ignore'}) const path = format({ dir: this._outputDir, name: fileName, ext: '.html' }) await promisify(fs.writeFile)(path, content) diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index f6e04f0..748335d 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -1,11 +1,31 @@ +import { fs, vol } from 'memfs' +const cosmiconfig = require('cosmiconfig') + import Generate from '../../src/commands/generate' +jest.mock('cosmiconfig') + describe('Generate', () => { + const CURRENT_DIR: string = process.cwd() + + beforeEach(() => { + vol.fromJSON({ + './concordialang-ui-html.json': '{}' + }, CURRENT_DIR) + + const explorer = { + loadSync: () => ({ + config: vol.readFileSync(`${ CURRENT_DIR }/concordialang-ui-html.json`, 'utf8') + }) + } + cosmiconfig.mockReturnValue(explorer) + }) + it('should print a JSON content', async () => { let spy = jest.spyOn(process.stdout, 'write'); - await Generate.run(['--features', '[]', '--outputDir', 'outputDir']) - expect(spy).not.toBeCalled() + await Generate.run(['--features', '{ "features": [] }', '--outputDir', 'outputDir']) + expect(spy).toBeCalledWith("No features found\n") }) }) diff --git a/test/generator.spec.ts b/test/html-ui-prototyper.spec.ts similarity index 67% rename from test/generator.spec.ts rename to test/html-ui-prototyper.spec.ts index b917c69..698ae9a 100644 --- a/test/generator.spec.ts +++ b/test/html-ui-prototyper.spec.ts @@ -2,19 +2,35 @@ import { Feature } from 'concordialang-ui-core' import { minify } from 'html-minifier' import { fs, vol } from 'memfs' import { promisify } from 'util' +const cosmiconfig = require('cosmiconfig') import HtmlUIPrototyper from '../src/html-ui-prototyper' +jest.mock('cosmiconfig') + describe('HtmlUIPrototyper', () => { const CURRENT_DIR: string = process.cwd() - let prototyper: HtmlUIPrototyper | null + const appConfig = { + widgets: { + input: {}, + label: {} + } + } beforeEach(() => { vol.fromJSON({ - '/concordialang-ui-html.json': '{}' + './concordialang-ui-html.json': JSON.stringify(appConfig) }, CURRENT_DIR) + + const explorer = { + loadSync: () => ({ + config: vol.readFileSync(`${ CURRENT_DIR }/concordialang-ui-html.json`, 'utf8') + }) + } + cosmiconfig.mockReturnValue(explorer) + prototyper = new HtmlUIPrototyper(fs, CURRENT_DIR) // In-memory fs }) @@ -43,8 +59,10 @@ describe('HtmlUIPrototyper', () => { expect(produced).toEqual(expected) } - it('produces an HTML file from features', async () => { - const features: Feature[] = [ /* something here */ ] + // FIXME the content of app config file is a string. It should be an object. + // Maybe we will have to mock loadJson from cosmiconfig. + xit('produces an HTML file from features', async () => { + const features: Feature[] = [ { name: 'Test Feature', uiElements: [ { name: 'Name', widget: 'textbox', props: { id: 'name' }, position: 0 } ], position: 0 } ] const htmls: string[] = [ /* put the expected html here */]; await expectFeaturesToProduceHtml(features, htmls) }) From 4f53e7abe80119672c414f1f39cbbba523afe2f8 Mon Sep 17 00:00:00 2001 From: Willian Date: Tue, 2 Jul 2019 01:24:14 -0300 Subject: [PATCH 6/9] Update dist files --- dist/commands/generate.js | 2 +- dist/html-ui-prototyper.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dist/commands/generate.js b/dist/commands/generate.js index 93cad7e..c4a6d94 100644 --- a/dist/commands/generate.js +++ b/dist/commands/generate.js @@ -15,7 +15,7 @@ class Generate extends command_1.Command { flags.outputDir ) const result = yield generator.generate(processResult.features) - this.log(JSON.stringify(result)) + this.log(result.join('\n')) } }) } diff --git a/dist/html-ui-prototyper.js b/dist/html-ui-prototyper.js index 1b3ad06..1a378e9 100644 --- a/dist/html-ui-prototyper.js +++ b/dist/html-ui-prototyper.js @@ -16,6 +16,8 @@ class HtmlUIPrototyper { return tslib_1.__awaiter(this, void 0, void 0, function*() { const appConfig = this.getAppConfig() const factory = new widget_factory_1.default(appConfig) + if (features.length === 0) + return Promise.resolve(['No features found']) const createFilePromises = [] for (let feature of features) { const elements = feature.uiElements.map(uiElement => @@ -33,7 +35,7 @@ class HtmlUIPrototyper { let content = widgets.reduce((result, widget) => { return result + widget.renderToString() }, '') - content = prettier.format(`
\n${content}
`, { + content = prettier.format(`
${content}
`, { parser: 'html', htmlWhitespaceSensitivity: 'ignore', }) From 86c078aaac56bd4658636e87aee051b0fa3cce45 Mon Sep 17 00:00:00 2001 From: Willian Date: Sat, 6 Jul 2019 00:54:48 -0300 Subject: [PATCH 7/9] Add tests for generate command --- __mocks__/cosmiconfig.ts | 13 ++++ __mocks__/fs.ts | 11 ++++ __mocks__/util.ts | 13 ++++ dist/utils/case-converter.d.ts | 1 + dist/utils/case-converter.js | 30 +++++++++ package-lock.json | 5 ++ package.json | 4 +- src/commands/generate.ts | 37 ++++++----- src/html-ui-prototyper.ts | 8 ++- src/interfaces/app-config.ts | 13 +--- src/utils/case-converter.ts | 18 ++++++ src/utils/prop.ts | 6 +- test/commands/generate.spec.ts | 115 +++++++++++++++++++++++++++------ test/fixtures/app-config.ts | 17 +++++ test/fixtures/features.ts | 32 +++++++++ 15 files changed, 268 insertions(+), 55 deletions(-) create mode 100644 __mocks__/cosmiconfig.ts create mode 100644 __mocks__/fs.ts create mode 100644 __mocks__/util.ts create mode 100644 dist/utils/case-converter.d.ts create mode 100644 dist/utils/case-converter.js create mode 100644 src/utils/case-converter.ts create mode 100644 test/fixtures/app-config.ts create mode 100644 test/fixtures/features.ts diff --git a/__mocks__/cosmiconfig.ts b/__mocks__/cosmiconfig.ts new file mode 100644 index 0000000..03bee8a --- /dev/null +++ b/__mocks__/cosmiconfig.ts @@ -0,0 +1,13 @@ +import { vol } from 'memfs' + +const loadConfigFile = (filePath) => { + const fileContent: string = vol.readFileSync(filePath, 'utf8') as string + const config = JSON.parse(fileContent) + return { config } +} + +const explorer = { loadSync: loadConfigFile } + +const cosmiconfig = jest.fn(() => explorer) + +module.exports = cosmiconfig diff --git a/__mocks__/fs.ts b/__mocks__/fs.ts new file mode 100644 index 0000000..294b1ae --- /dev/null +++ b/__mocks__/fs.ts @@ -0,0 +1,11 @@ +import { vol } from 'memfs' + +const fs = jest.requireActual('fs') + +const mockedWriteFile = (path: string, content: string) => { + vol.writeFileSync(path, content) +} + +jest.spyOn(fs, 'writeFile').mockImplementation(mockedWriteFile) + +module.exports = fs diff --git a/__mocks__/util.ts b/__mocks__/util.ts new file mode 100644 index 0000000..e557fa6 --- /dev/null +++ b/__mocks__/util.ts @@ -0,0 +1,13 @@ +const util = jest.requireActual('util') + +function promisify(fn) { + return (...args) => { + return new Promise(resolve => { + resolve(fn(...args)) + }) + } +} + +util.promisify = promisify + +module.exports = util diff --git a/dist/utils/case-converter.d.ts b/dist/utils/case-converter.d.ts new file mode 100644 index 0000000..fff91a0 --- /dev/null +++ b/dist/utils/case-converter.d.ts @@ -0,0 +1 @@ +export declare function convertCase(text: string, type: string): string; diff --git a/dist/utils/case-converter.js b/dist/utils/case-converter.js new file mode 100644 index 0000000..175a1a6 --- /dev/null +++ b/dist/utils/case-converter.js @@ -0,0 +1,30 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const case_1 = require('case') +var CaseType +;(function(CaseType) { + CaseType['CAMEL'] = 'camel' + CaseType['PASCAL'] = 'pascal' + CaseType['SNAKE'] = 'snake' + CaseType['KEBAB'] = 'kebab' +})(CaseType || (CaseType = {})) +function convertCase(text, type) { + switch ( + type + .toString() + .trim() + .toLowerCase() + ) { + case CaseType.CAMEL: + return case_1.camel(text) + case CaseType.PASCAL: + return case_1.pascal(text) + case CaseType.SNAKE: + return case_1.snake(text) + case CaseType.KEBAB: + return case_1.kebab(text) + default: + return text // do nothing + } +} +exports.convertCase = convertCase diff --git a/package-lock.json b/package-lock.json index e4ee447..8f06b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5448,6 +5448,11 @@ "which": "^1.3.0" } }, + "normalize-diacritics": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-diacritics/-/normalize-diacritics-2.5.0.tgz", + "integrity": "sha512-E6A4zGUh1MIhFnNb0WolW+NqCHPyiKfuPRikNDOt1RuLjJ+7aIZU30d+h7sbZGNT78mbR12Y+0FNgUTY/7IWHw==" + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", diff --git a/package.json b/package.json index 4078409..b922562 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "ts-jest": { "diagnostics": false } - } + }, + "testEnvironment": "node" }, "oclif": { "commands": "./dist/commands", @@ -77,6 +78,7 @@ "concordialang-ui-core": "^0.2.3", "cosmiconfig": "^5.2.1", "lodash": "^4.17.11", + "normalize-diacritics": "^2.5.0", "tslib": "^1.9.3" }, "devDependencies": { diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 51468d9..54993ba 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -1,27 +1,32 @@ import {Command, flags} from '@oclif/command' -import {ProcessResult} from 'concordialang-ui-core' +import {ProcessResult, Feature} from 'concordialang-ui-core' import * as fs from 'fs' import HtmlUIPrototyper from '../html-ui-prototyper' export default class Generate extends Command { - static description = 'Generate html files' + static description = 'Generate html files' - static flags = { - help: flags.help({char: 'h'}), - features: flags.string({description: 'processed features from ast', required: true}), - outputDir: flags.string({description: 'location where output files will be saved', required: true}) - } + static flags = { + help: flags.help({char: 'h'}), + features: flags.string({description: 'processed features from ast', required: true}), + outputDir: flags.string({description: 'location where output files will be saved', required: true}) + } - async run() { - const {flags} = this.parse(Generate) + async run() { + try { + const {flags} = this.parse(Generate) + if (!flags.features) throw new Error('Missing flag --features') - if (flags.features) { - const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult - const generator = new HtmlUIPrototyper(fs, flags.outputDir) - const result = await generator.generate(processResult.features) - this.log(result.join('\n')) - } - } + const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult + if (processResult.features.length === 0) throw new Error('No features found') + + const generator = new HtmlUIPrototyper(fs, flags.outputDir) + const result = await generator.generate(processResult.features) + this.log(result.join('\n')) + } catch(e) { + this.log(e.message) + } + } } diff --git a/src/html-ui-prototyper.ts b/src/html-ui-prototyper.ts index 77f443a..3111166 100644 --- a/src/html-ui-prototyper.ts +++ b/src/html-ui-prototyper.ts @@ -2,8 +2,11 @@ import { Feature, Prototyper, Widget } from 'concordialang-ui-core' import * as fs from 'fs' import { promisify } from 'util' import { format } from 'path' +import { convertCase } from './utils/case-converter' + const prettier = require('prettier') const cosmiconfig = require('cosmiconfig') +const { normalize } = require('normalize-diacritics') import WidgetFactory from './widgets/widget-factory' import { AppConfig } from './interfaces/app-config' @@ -15,9 +18,6 @@ export default class HtmlUIPrototyper implements Prototyper { public async generate(features: Feature[]): Promise { const appConfig: AppConfig = this.getAppConfig() const factory = new WidgetFactory(appConfig) - - if (features.length === 0) return Promise.resolve([ 'No features found' ]) - const createFilePromises: Promise[] = [] for (let feature of features) { @@ -29,6 +29,8 @@ export default class HtmlUIPrototyper implements Prototyper { } private async createHtmlFile(fileName: string, widgets: Widget[]): Promise { + fileName = await normalize(convertCase(fileName, 'snake')) + let content = widgets.reduce((result, widget) => { return result + widget.renderToString() }, '') diff --git a/src/interfaces/app-config.ts b/src/interfaces/app-config.ts index b462f6f..64f90f0 100644 --- a/src/interfaces/app-config.ts +++ b/src/interfaces/app-config.ts @@ -1,10 +1,6 @@ export interface AppConfig { widgets?: { - input?: WidgetConfig, - radio?: WidgetConfig, - checkbox?: WidgetConfig, - select?: WidgetConfig, - label?: LabelConfig + [key: string]: WidgetConfig, } } @@ -15,10 +11,5 @@ export interface WidgetConfig { optionClosure?: string, wrapperOpening?: string, wrapperClosure?: string, - label?: LabelConfig -} - -interface LabelConfig { - opening: string, - closure: string + label?: WidgetConfig } diff --git a/src/utils/case-converter.ts b/src/utils/case-converter.ts new file mode 100644 index 0000000..8f2dbbd --- /dev/null +++ b/src/utils/case-converter.ts @@ -0,0 +1,18 @@ +import { camel, pascal, snake, kebab } from 'case'; + +enum CaseType { + CAMEL = 'camel', + PASCAL = 'pascal', + SNAKE = 'snake', + KEBAB = 'kebab' +} + +export function convertCase(text: string, type: string): string { + switch (type.toString().trim().toLowerCase()) { + case CaseType.CAMEL: return camel(text); + case CaseType.PASCAL: return pascal(text); + case CaseType.SNAKE: return snake(text); + case CaseType.KEBAB: return kebab(text); + default: return text; // do nothing + } +} diff --git a/src/utils/prop.ts b/src/utils/prop.ts index 15d322f..1ec42fd 100644 --- a/src/utils/prop.ts +++ b/src/utils/prop.ts @@ -1,8 +1,8 @@ -import { camel } from 'case' +import { convertCase } from './case-converter' export const PROPS_INJECTION_POINT = '%s' -export function formatProperties(props: any, validProperties: string[]): string { +export function formatProperties(props: any, validProperties: string[], caseType: string = 'camel'): string { const translateProp = (key: string) => { switch(key) { case 'format': return 'pattern'; @@ -11,7 +11,7 @@ export function formatProperties(props: any, validProperties: string[]): string } const getFormattedProp = (key: string) => { - let value = camel(props[key].toString()) + let value = convertCase(props[key].toString(), caseType) return `${translateProp(key)}="${value}"` } diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index 748335d..7a3dea5 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -1,31 +1,104 @@ -import { fs, vol } from 'memfs' -const cosmiconfig = require('cosmiconfig') - +import { fs as memfs, vol } from 'memfs' import Generate from '../../src/commands/generate' +import { completeAppConfig } from '../fixtures/app-config' +import { featureWithName } from '../fixtures/features' -jest.mock('cosmiconfig') +jest.mock('fs') +jest.mock('util') describe('Generate', () => { + const CURRENT_DIR: string = process.cwd() + const OUTPUT_DIR: string = 'outputDir' + + const mockFiles = (files) => { vol.fromJSON(files, CURRENT_DIR) } + + afterAll(() => { + require('fs').writeFile.mockRestore() + }) + + describe('with a complete app config', () => { + let spy + + beforeAll(async () => { + vol.reset() + mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) + vol.mkdirSync(OUTPUT_DIR) + + spy = jest.spyOn(process.stdout, 'write'); + const features: string = JSON.stringify(featureWithName('Login de usuário')) + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) + + it('should save an html file', () => { + expect(require('fs').writeFile).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html`, expect.anything()) + expect(memfs.existsSync(`${OUTPUT_DIR}/login_de_usuario.html`)).toBe(true) + }) + + it('should list the generated file in the console', () => { + expect(spy).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html\n`) + }) + }) + + describe('without features', () => { + let spy + + beforeAll(async () => { + vol.reset() + mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) + vol.mkdirSync(OUTPUT_DIR) + + spy = jest.spyOn(process.stdout, 'write'); + const features: string = '{ "features": [] }' + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) + + it('should show an error message', async () => { + expect(spy).toBeCalledWith(expect.stringContaining("No features found")) + }) + + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) + + describe('without the outputDir flag', () => { + let spy + + beforeAll(async () => { + vol.reset() + mockFiles({ 'concordialang-ui-html.json': completeAppConfig }) + + spy = jest.spyOn(process.stdout, 'write'); + const features: string = JSON.stringify(featureWithName('Login de usuário')) + await Generate.run(['--features', features]) + }) + + it('should show an error message', async () => { + expect(spy).toBeCalledWith(expect.stringContaining("Missing required flag")) + }) + + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) - const CURRENT_DIR: string = process.cwd() + describe('without an app config', () => { + let spy - beforeEach(() => { - vol.fromJSON({ - './concordialang-ui-html.json': '{}' - }, CURRENT_DIR) + beforeAll(async () => { + vol.reset() - const explorer = { - loadSync: () => ({ - config: vol.readFileSync(`${ CURRENT_DIR }/concordialang-ui-html.json`, 'utf8') - }) - } - cosmiconfig.mockReturnValue(explorer) - }) + spy = jest.spyOn(process.stdout, 'write'); + const features: string = '{"features":[{"name":"Login de usuário","position":2,"uiElements":[{"name":"Nome de Usuário","widget":"textbox","position":22,"props":{"id":"nome_usuario"}},{"name":"Senha","widget":"textbox","position":26,"props":{"id":"senha","required":true}},{"name":"Entrar","widget":"button","position":31,"props":{}}]}]}' + await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) + }) - it('should print a JSON content', async () => { - let spy = jest.spyOn(process.stdout, 'write'); - await Generate.run(['--features', '{ "features": [] }', '--outputDir', 'outputDir']) - expect(spy).toBeCalledWith("No features found\n") - }) + it('should show an error message', async () => { + expect(spy).toBeCalledWith(expect.stringContaining("Config file not found")) + }) + it('should not write any file', () => { + expect(require('fs').writeFile).not.toBeCalled + }) + }) }) diff --git a/test/fixtures/app-config.ts b/test/fixtures/app-config.ts new file mode 100644 index 0000000..e93a3bd --- /dev/null +++ b/test/fixtures/app-config.ts @@ -0,0 +1,17 @@ +export const completeAppConfig: string = JSON.stringify({ + widgets: { + input: { + opening: '', + wrapperOpening: '
', + wrapperClosure: '
' + }, + label: { + opening: '' + }, + button: { + opening: '' + } + } +}) diff --git a/test/fixtures/features.ts b/test/fixtures/features.ts new file mode 100644 index 0000000..6bf8bde --- /dev/null +++ b/test/fixtures/features.ts @@ -0,0 +1,32 @@ +export const featureWithName = (name) => ({ + features: [ + { + name, + position: 2, + uiElements: [ + { + name: "Nome de Usuário", + widget: "textbox", + position: 22, + props: { + id: "nome_usuario"} + }, + { + name: "Senha", + widget: "textbox", + position: 26, + props: { + id: "senha", + required: true + } + }, + { + name: "Entrar", + widget: "button", + position: 31, + props: {} + } + ] + } + ] +}) From bf06cd3614b0953555d06b9e1a33bbd51c861163 Mon Sep 17 00:00:00 2001 From: Willian Date: Mon, 15 Jul 2019 18:00:17 -0300 Subject: [PATCH 8/9] Update app-config and widgets rendering --- __mocks__/cosmiconfig.ts | 2 +- __mocks__/fs.ts | 2 +- dist/commands/generate.js | 9 ++- dist/html-ui-prototyper.js | 14 ++-- dist/interfaces/app-config.d.ts | 36 +++++----- dist/utils/format-html.d.ts | 1 + dist/utils/format-html.js | 10 +++ dist/utils/index.d.ts | 3 + dist/utils/index.js | 6 ++ dist/utils/prop.d.ts | 3 +- dist/utils/prop.js | 21 +++--- dist/widgets/button.d.ts | 10 ++- dist/widgets/button.js | 34 ++++----- dist/widgets/checkbox.d.ts | 10 ++- dist/widgets/checkbox.js | 35 ++++----- dist/widgets/html-widget.d.ts | 11 +++ dist/widgets/html-widget.js | 95 +++++++++++++++++++++++++ dist/widgets/input.d.ts | 10 ++- dist/widgets/input.js | 43 +++++------ dist/widgets/label.d.ts | 2 +- dist/widgets/label.js | 20 +++--- dist/widgets/radio.d.ts | 10 ++- dist/widgets/radio.js | 44 ++++-------- dist/widgets/select.d.ts | 13 ++-- dist/widgets/select.js | 57 ++++----------- dist/widgets/widget-factory.js | 14 ++-- dist/widgets/wrapper.js | 7 +- package-lock.json | 10 +++ package.json | 2 + src/html-ui-prototyper.ts | 4 +- src/interfaces/app-config.ts | 26 +++++-- src/utils/format-html.ts | 5 ++ src/utils/index.ts | 3 + src/utils/prop.ts | 19 ++--- src/widgets/button.ts | 33 +++++---- src/widgets/checkbox.ts | 31 ++++---- src/widgets/html-widget.ts | 94 ++++++++++++++++++++++++ src/widgets/input.ts | 46 ++++++------ src/widgets/label.ts | 13 ++-- src/widgets/radio.ts | 41 ++++------- src/widgets/select.ts | 47 +++++------- src/widgets/widget-factory.ts | 17 ++--- src/widgets/wrapper.ts | 5 +- test/commands/generate.spec.ts | 33 ++++----- test/fixtures/app-config.ts | 27 ++++--- test/utils/format-properties.spec.ts | 19 ++--- test/widgets/button.spec.ts | 49 +++++++------ test/widgets/checkbox.spec.ts | 64 +++++++++++------ test/widgets/input.spec.ts | 72 ++++++++++--------- test/widgets/prop.spec.ts | 9 --- test/widgets/radio.spec.ts | 60 ++++++++++------ test/widgets/select.spec.ts | 64 ++++++++++------- test/widgets/widget-factory.spec.ts | 102 ++++++++++++--------------- tslint.json | 9 ++- 54 files changed, 815 insertions(+), 611 deletions(-) create mode 100644 dist/utils/format-html.d.ts create mode 100644 dist/utils/format-html.js create mode 100644 dist/utils/index.d.ts create mode 100644 dist/utils/index.js create mode 100644 dist/widgets/html-widget.d.ts create mode 100644 dist/widgets/html-widget.js create mode 100644 src/utils/format-html.ts create mode 100644 src/utils/index.ts create mode 100644 src/widgets/html-widget.ts delete mode 100644 test/widgets/prop.spec.ts diff --git a/__mocks__/cosmiconfig.ts b/__mocks__/cosmiconfig.ts index 03bee8a..de7f103 100644 --- a/__mocks__/cosmiconfig.ts +++ b/__mocks__/cosmiconfig.ts @@ -1,6 +1,6 @@ import { vol } from 'memfs' -const loadConfigFile = (filePath) => { +const loadConfigFile = filePath => { const fileContent: string = vol.readFileSync(filePath, 'utf8') as string const config = JSON.parse(fileContent) return { config } diff --git a/__mocks__/fs.ts b/__mocks__/fs.ts index 294b1ae..8768c47 100644 --- a/__mocks__/fs.ts +++ b/__mocks__/fs.ts @@ -2,7 +2,7 @@ import { vol } from 'memfs' const fs = jest.requireActual('fs') -const mockedWriteFile = (path: string, content: string) => { +function mockedWriteFile(path: string, content: string) { vol.writeFileSync(path, content) } diff --git a/dist/commands/generate.js b/dist/commands/generate.js index c4a6d94..99ba489 100644 --- a/dist/commands/generate.js +++ b/dist/commands/generate.js @@ -7,15 +7,20 @@ const html_ui_prototyper_1 = require('../html-ui-prototyper') class Generate extends command_1.Command { run() { return tslib_1.__awaiter(this, void 0, void 0, function*() { - const { flags } = this.parse(Generate) - if (flags.features) { + try { + const { flags } = this.parse(Generate) + if (!flags.features) throw new Error('Missing flag --features') const processResult = JSON.parse(flags.features) + if (processResult.features.length === 0) + throw new Error('No features found') const generator = new html_ui_prototyper_1.default( fs, flags.outputDir ) const result = yield generator.generate(processResult.features) this.log(result.join('\n')) + } catch (e) { + this.log(e.message) } }) } diff --git a/dist/html-ui-prototyper.js b/dist/html-ui-prototyper.js index 1a378e9..b59a7d2 100644 --- a/dist/html-ui-prototyper.js +++ b/dist/html-ui-prototyper.js @@ -4,8 +4,10 @@ const tslib_1 = require('tslib') const fs = require('fs') const util_1 = require('util') const path_1 = require('path') -const prettier = require('prettier') +const case_converter_1 = require('./utils/case-converter') +const format_html_1 = require('./utils/format-html') const cosmiconfig = require('cosmiconfig') +const { normalize } = require('normalize-diacritics') const widget_factory_1 = require('./widgets/widget-factory') class HtmlUIPrototyper { constructor(_fs = fs, _outputDir) { @@ -16,8 +18,6 @@ class HtmlUIPrototyper { return tslib_1.__awaiter(this, void 0, void 0, function*() { const appConfig = this.getAppConfig() const factory = new widget_factory_1.default(appConfig) - if (features.length === 0) - return Promise.resolve(['No features found']) const createFilePromises = [] for (let feature of features) { const elements = feature.uiElements.map(uiElement => @@ -32,13 +32,13 @@ class HtmlUIPrototyper { } createHtmlFile(fileName, widgets) { return tslib_1.__awaiter(this, void 0, void 0, function*() { + fileName = yield normalize( + case_converter_1.convertCase(fileName, 'snake') + ) let content = widgets.reduce((result, widget) => { return result + widget.renderToString() }, '') - content = prettier.format(`
${content}
`, { - parser: 'html', - htmlWhitespaceSensitivity: 'ignore', - }) + content = format_html_1.formatHtml(`
${content}
`) const path = path_1.format({ dir: this._outputDir, name: fileName, diff --git a/dist/interfaces/app-config.d.ts b/dist/interfaces/app-config.d.ts index 9f58ef3..9d65b8d 100644 --- a/dist/interfaces/app-config.d.ts +++ b/dist/interfaces/app-config.d.ts @@ -1,23 +1,25 @@ export interface AppConfig { widgets?: { - input?: WidgetConfig; - radio?: WidgetConfig; - checkbox?: WidgetConfig; - select?: WidgetConfig; - label?: LabelConfig; + [key: string]: WidgetConfig; }; } export interface WidgetConfig { - opening: string; - closure?: string; - optionOpening?: string; - optionClosure?: string; - wrapperOpening?: string; - wrapperClosure?: string; - label?: LabelConfig; -} -interface LabelConfig { - opening: string; - closure: string; + template?: string; + widget: { + opening: string; + closure?: string; + onePerValue?: boolean; + }; + valueWrapper?: { + opening: string; + closure: string; + }; + wrapper?: { + opening: string; + closure: string; + }; + label?: { + opening: string; + closure: string; + }; } -export {}; diff --git a/dist/utils/format-html.d.ts b/dist/utils/format-html.d.ts new file mode 100644 index 0000000..a12da9d --- /dev/null +++ b/dist/utils/format-html.d.ts @@ -0,0 +1 @@ +export declare function formatHtml(html: string): any; diff --git a/dist/utils/format-html.js b/dist/utils/format-html.js new file mode 100644 index 0000000..eb4b05f --- /dev/null +++ b/dist/utils/format-html.js @@ -0,0 +1,10 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const prettier = require('prettier') +function formatHtml(html) { + return prettier.format(html, { + parser: 'html', + htmlWhitespaceSensitivity: 'ignore', + }) +} +exports.formatHtml = formatHtml diff --git a/dist/utils/index.d.ts b/dist/utils/index.d.ts new file mode 100644 index 0000000..5a535f2 --- /dev/null +++ b/dist/utils/index.d.ts @@ -0,0 +1,3 @@ +export * from './case-converter'; +export * from './format-html'; +export * from './prop'; diff --git a/dist/utils/index.js b/dist/utils/index.js new file mode 100644 index 0000000..6d61023 --- /dev/null +++ b/dist/utils/index.js @@ -0,0 +1,6 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const tslib_1 = require('tslib') +tslib_1.__exportStar(require('./case-converter'), exports) +tslib_1.__exportStar(require('./format-html'), exports) +tslib_1.__exportStar(require('./prop'), exports) diff --git a/dist/utils/prop.d.ts b/dist/utils/prop.d.ts index 9934699..c34767e 100644 --- a/dist/utils/prop.d.ts +++ b/dist/utils/prop.d.ts @@ -1,2 +1 @@ -export declare const PROPS_INJECTION_POINT = "%s"; -export declare function formatProperties(props: any, validProperties: string[]): string; +export declare function formatProperties(props: any, caseType?: string): string; diff --git a/dist/utils/prop.js b/dist/utils/prop.js index b4aede4..7f57fef 100644 --- a/dist/utils/prop.js +++ b/dist/utils/prop.js @@ -1,8 +1,7 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const case_1 = require('case') -exports.PROPS_INJECTION_POINT = '%s' -function formatProperties(props, validProperties) { +const case_converter_1 = require('./case-converter') +function formatProperties(props, caseType = 'camel') { const translateProp = key => { switch (key) { case 'format': @@ -11,17 +10,15 @@ function formatProperties(props, validProperties) { return key } } - const getFormattedProp = key => { - let value = case_1.camel(props[key].toString()) - return `${translateProp(key)}="${value}"` - } - const formatValid = (result, prop) => { - return validProperties.includes(prop) - ? result + getFormattedProp(prop) + ' ' - : result + const getValueOf = key => + case_converter_1.convertCase(props[key].toString(), caseType) + const format = (result, key) => { + const value = getValueOf(key) + const htmlProp = translateProp(key) + return result + `${htmlProp}="${value}"` + ' ' } return Object.keys(props) - .reduce(formatValid, '') + .reduce(format, '') .trimRight() } exports.formatProperties = formatProperties diff --git a/dist/widgets/button.d.ts b/dist/widgets/button.d.ts index 22f036a..53ef2a2 100644 --- a/dist/widgets/button.d.ts +++ b/dist/widgets/button.d.ts @@ -1,9 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Button extends Widget { - private _config; - private readonly VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; +import HtmlWidget from './html-widget'; +export default class Button extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; private getType; } diff --git a/dist/widgets/button.js b/dist/widgets/button.js index d77d82d..33c5313 100644 --- a/dist/widgets/button.js +++ b/dist/widgets/button.js @@ -1,29 +1,23 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -class Button extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name || '') - this._config = _config - this.VALID_PROPERTIES = ['id', 'disabled', 'value'] +const html_widget_1 = require('./html-widget') +class Button extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + this.props.value = this.props.value || name } - renderToString() { - const buttonType = this.getType(this.props.datatype) - let properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - properties = `${buttonType} ${properties}` - const buttonOpening = this._config.opening.replace( - prop_1.PROPS_INJECTION_POINT, - properties - ) - const buttonClosure = this._config.closure - return buttonOpening + this.name + buttonClosure + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'disabled'] + props.type = this.getType(props.datatype) + props.value = props.value || this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } getType(datatype) { - return `type="${datatype || 'button'}"` + return datatype || 'button' } } exports.default = Button diff --git a/dist/widgets/checkbox.d.ts b/dist/widgets/checkbox.d.ts index 8128427..9b4fc7a 100644 --- a/dist/widgets/checkbox.d.ts +++ b/dist/widgets/checkbox.d.ts @@ -1,8 +1,6 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Checkbox extends Widget { - private _config; - private readonly VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; +import HtmlWidget from './html-widget'; +export default class Checkbox extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/checkbox.js b/dist/widgets/checkbox.js index 4762708..1218466 100644 --- a/dist/widgets/checkbox.js +++ b/dist/widgets/checkbox.js @@ -1,30 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const wrapper_1 = require('./wrapper') -class Checkbox extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name) - this._config = _config - this.VALID_PROPERTIES = ['value', 'required'] +const html_widget_1 = require('./html-widget') +class Checkbox extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) } - renderToString() { - const inputType = 'type="checkbox"' - let properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - properties = `${inputType} ${properties}` - const inputOpening = this._config.opening.replace( - prop_1.PROPS_INJECTION_POINT, - properties - ) - const inputClosure = this._config.closure || '' - return wrapper_1.wrap( - inputOpening + this.name + inputClosure, - this._config - ) + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value', 'required'] + props.type = 'checkbox' + props.name = props.value + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Checkbox diff --git a/dist/widgets/html-widget.d.ts b/dist/widgets/html-widget.d.ts new file mode 100644 index 0000000..8056e13 --- /dev/null +++ b/dist/widgets/html-widget.d.ts @@ -0,0 +1,11 @@ +import { Widget } from 'concordialang-ui-core'; +import { WidgetConfig } from '../interfaces/app-config'; +export default abstract class HtmlWidget extends Widget { + private _config; + constructor(props: any, name: string, _config: WidgetConfig); + renderToString(): string; + protected abstract getFormattedProps(props: any): string; + private renderWidgetWithSingleValue; + private renderOneWidgetPerValue; + private renderWidgetWithMultipleValues; +} diff --git a/dist/widgets/html-widget.js b/dist/widgets/html-widget.js new file mode 100644 index 0000000..a4f01a4 --- /dev/null +++ b/dist/widgets/html-widget.js @@ -0,0 +1,95 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const concordialang_ui_core_1 = require('concordialang-ui-core') +const Mustache = require('mustache') +const prop_1 = require('../utils/prop') +const label_1 = require('./label') +const wrapper_1 = require('./wrapper') +class HtmlWidget extends concordialang_ui_core_1.Widget { + constructor(props, name, _config) { + super(props, name) + this._config = _config + } + renderToString() { + let main + if (Array.isArray(this.props.value)) { + main = this._config.widget.onePerValue + ? this.renderOneWidgetPerValue(this.props.value) + : this.renderWidgetWithMultipleValues(this.props.value) + } else { + main = this.renderWidgetWithSingleValue() + } + const widgetId = this.props.id ? this.props.id.toString() : undefined + const label = label_1.createLabel(this.name, widgetId, this._config) + return wrapper_1.wrap(label + main, this._config) + } + renderWidgetWithSingleValue() { + const props = this.getFormattedProps(this.props) + const config = { + widget: Object.assign({}, this._config.widget), + // Widgets like button may include {{value}} in the template + value: this.props.value, + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = + this._config.template || '{{&widget.opening}}{{&widget.closure}}' + return Mustache.render(template, config) + } + renderOneWidgetPerValue(values) { + // When rendering one widget per value, we must delete the "id" property + // to avoid multiple widgets with the same id. + delete this.props.id + const widgets = [] + for (const value of values) { + const props = this.getFormattedProps( + Object.assign({}, this.props, { value }) + ) + const config = { + widget: Object.assign({}, this._config.widget), + valueWrapper: Object.assign({}, this._config.valueWrapper), + value, + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = + this._config.template || + '{{&widget.opening}}{{&widget.closure}}{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgets.push(Mustache.render(template, config)) + } + return widgets.join(' ') + } + renderWidgetWithMultipleValues(values) { + const widgetValues = [] + for (const value of values) { + const props = prop_1.formatProperties({ value }) + const config = { + valueWrapper: this._config.valueWrapper + ? Object.assign({}, this._config.valueWrapper) + : { opening: '', closure: '' }, + value, + } + config.valueWrapper.opening = Mustache.render( + config.valueWrapper.opening, + { props } + ) + const template = + '{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgetValues.push(Mustache.render(template, config)) + } + delete this.props.value + const props = this.getFormattedProps(this.props) + const config = { + widget: Object.assign({}, this._config.widget), + values: widgetValues.join(' '), + } + config.widget.opening = Mustache.render(config.widget.opening, { + props, + }) + const template = '{{&widget.opening}}{{&values}}{{&widget.closure}}' + return Mustache.render(template, config) + } +} +exports.default = HtmlWidget diff --git a/dist/widgets/input.d.ts b/dist/widgets/input.d.ts index e95736b..d2669e0 100644 --- a/dist/widgets/input.d.ts +++ b/dist/widgets/input.d.ts @@ -1,9 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Input extends Widget { - private _config; - private readonly VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; +import HtmlWidget from './html-widget'; +export default class Input extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; private getType; } diff --git a/dist/widgets/input.js b/dist/widgets/input.js index 3a81976..5f64209 100644 --- a/dist/widgets/input.js +++ b/dist/widgets/input.js @@ -1,39 +1,28 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -const wrapper_1 = require('./wrapper') -class Input extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name) - this._config = _config - this.VALID_PROPERTIES = [ +const html_widget_1 = require('./html-widget') +class Input extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + } + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = [ 'id', + 'type', + 'name', 'editable', 'minlength', 'maxlength', 'required', 'format', ] - } - renderToString() { - const inputType = this.getType(this.props.datatype) - const properties = prop_1.formatProperties( - this.props, - this.VALID_PROPERTIES - ) - const inputOpening = this._config.opening.replace( - prop_1.PROPS_INJECTION_POINT, - `${inputType} ${properties}` - ) - const inputClosure = this._config.closure || '' - const label = label_1.createLabel( - this.name, - this.props.id.toString(), - this._config - ) - return wrapper_1.wrap(label + inputOpening + inputClosure, this._config) + props.type = this.getType(props.datatype) + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } getType(datatype) { let typeProperty @@ -51,7 +40,7 @@ class Input extends concordialang_ui_core_1.Widget { default: typeProperty = 'text' } - return `type="${typeProperty}"` + return typeProperty } } exports.default = Input diff --git a/dist/widgets/label.d.ts b/dist/widgets/label.d.ts index 483a09d..013d063 100644 --- a/dist/widgets/label.d.ts +++ b/dist/widgets/label.d.ts @@ -1,2 +1,2 @@ import { WidgetConfig } from '../interfaces/app-config'; -export declare function createLabel(widgetName: string, widgetId: string, widgetConfig: WidgetConfig): string; +export declare function createLabel(widgetName: string, widgetId: string | undefined, widgetConfig: WidgetConfig): string; diff --git a/dist/widgets/label.js b/dist/widgets/label.js index 5b8099c..7ae3d93 100644 --- a/dist/widgets/label.js +++ b/dist/widgets/label.js @@ -1,17 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const prop_1 = require('../utils/prop') +const Mustache = require('mustache') +const utils_1 = require('../utils') function createLabel(widgetName, widgetId, widgetConfig) { if (!widgetConfig.label) return '' const idPattern = /^(#|~|\d|\w).*/ - const labelFor = widgetId.match(idPattern) - ? `for="${widgetId.replace(/^#|~/, '')}"` - : '' - const labelOpening = widgetConfig.label.opening.replace( - prop_1.PROPS_INJECTION_POINT, - labelFor + const labelFor = + widgetId && widgetId.match(idPattern) + ? `for="${widgetId.replace(/^#|~/, '')}"` + : '' + widgetConfig.label.opening = Mustache.render(widgetConfig.label.opening, { + props: labelFor, + }) + return utils_1.formatHtml( + widgetConfig.label.opening + widgetName + widgetConfig.label.closure ) - const labelClosure = widgetConfig.label.closure - return labelOpening + widgetName + labelClosure } exports.createLabel = createLabel diff --git a/dist/widgets/radio.d.ts b/dist/widgets/radio.d.ts index e7a0a80..2cc48ed 100644 --- a/dist/widgets/radio.d.ts +++ b/dist/widgets/radio.d.ts @@ -1,8 +1,6 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Radio extends Widget { - private _config; - private readonly VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; +import HtmlWidget from './html-widget'; +export default class Radio extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/radio.js b/dist/widgets/radio.js index c3b4012..86f1667 100644 --- a/dist/widgets/radio.js +++ b/dist/widgets/radio.js @@ -1,39 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -const wrapper_1 = require('./wrapper') -class Radio extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name) - this._config = _config - this.VALID_PROPERTIES = ['value'] +const html_widget_1 = require('./html-widget') +class Radio extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) } - renderToString() { - const inputType = 'type="radio"' - const label = label_1.createLabel(this.name, '', this._config) - let inputs = [] - for (let value of this.props.value) { - // TODO: o que fazer no formatProperties em relação ao value? - // provavelmente terei que instalar o pacote "case" - // para ter 'value="algumaCoisa"', quando value for "Alguma Coisa" - // - // TODO: adicionar propriedades 'id' e 'nome' - const props = Object.assign({}, this.props, { value }) - let properties = prop_1.formatProperties( - props, - this.VALID_PROPERTIES - ) - properties = `${inputType} ${properties}` - const inputOpening = this._config.opening.replace( - prop_1.PROPS_INJECTION_POINT, - properties - ) - const inputClosure = this._config.closure || '' - inputs.push(inputOpening + value + inputClosure) - } - return wrapper_1.wrap(label + inputs.join(''), this._config) + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value'] + props.type = 'radio' + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Radio diff --git a/dist/widgets/select.d.ts b/dist/widgets/select.d.ts index 1a81fc7..0d3dfd3 100644 --- a/dist/widgets/select.d.ts +++ b/dist/widgets/select.d.ts @@ -1,10 +1,7 @@ -import { Widget } from 'concordialang-ui-core'; import { WidgetConfig } from '../interfaces/app-config'; -export default class Select extends Widget { - private _config; - private readonly SELECT_VALID_PROPERTIES; - private readonly OPTION_VALID_PROPERTIES; - constructor(props: any, name: string, _config: WidgetConfig); - renderToString(): string; - private getOptions; +import HtmlWidget from './html-widget'; +export default class Select extends HtmlWidget { + private readonly VALID_PROPERTIES; + constructor(props: any, name: string, config: WidgetConfig); + protected getFormattedProps(props: any): string; } diff --git a/dist/widgets/select.js b/dist/widgets/select.js index f748755..00274ce 100644 --- a/dist/widgets/select.js +++ b/dist/widgets/select.js @@ -1,52 +1,19 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) -const concordialang_ui_core_1 = require('concordialang-ui-core') +const lodash_1 = require('lodash') const prop_1 = require('../utils/prop') -const label_1 = require('./label') -const wrapper_1 = require('./wrapper') -class Select extends concordialang_ui_core_1.Widget { - constructor(props, name, _config) { - super(props, name) - this._config = _config - this.SELECT_VALID_PROPERTIES = ['id', 'required'] - this.OPTION_VALID_PROPERTIES = ['value'] +const html_widget_1 = require('./html-widget') +class Select extends html_widget_1.default { + constructor(props, name, config) { + super(props, name, config) + this.VALID_PROPERTIES = [] } - renderToString() { - const properties = prop_1.formatProperties( - this.props, - this.SELECT_VALID_PROPERTIES - ) - const selectOpening = this._config.opening.replace( - prop_1.PROPS_INJECTION_POINT, - properties - ) - const selectClosure = this._config.closure - const options = this.getOptions() - const select = selectOpening + options + selectClosure - const label = label_1.createLabel( - this.name, - this.props.id.toString(), - this._config - ) - return wrapper_1.wrap(label + select, this._config) - } - getOptions() { - if (!this._config.optionOpening) return '' - let options = [] - for (let value of this.props.value) { - const optionProps = { value } - const properties = prop_1.formatProperties( - optionProps, - this.OPTION_VALID_PROPERTIES - ) - const optionOpening = this._config.optionOpening.replace( - prop_1.PROPS_INJECTION_POINT, - properties - ) - const optionClosure = this._config.optionClosure - options.push(optionOpening + value + optionClosure) - } - return options.join('') + getFormattedProps(props) { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'name', 'required'] + props.name = this.name + const filteredProps = lodash_1.pick(props, VALID_PROPERTIES) + return prop_1.formatProperties(filteredProps) } } exports.default = Select diff --git a/dist/widgets/widget-factory.js b/dist/widgets/widget-factory.js index c5dfd23..f91f996 100644 --- a/dist/widgets/widget-factory.js +++ b/dist/widgets/widget-factory.js @@ -2,8 +2,8 @@ Object.defineProperty(exports, '__esModule', { value: true }) const lodash_1 = require('lodash') const button_1 = require('./button') -const input_1 = require('./input') const checkbox_1 = require('./checkbox') +const input_1 = require('./input') const radio_1 = require('./radio') const select_1 = require('./select') class WidgetFactory { @@ -29,25 +29,29 @@ class WidgetFactory { createInputElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.input') widgetConfig.label = - widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') return new input_1.default(element.props, element.name, widgetConfig) } createRadioElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.radio') widgetConfig.label = - widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') return new radio_1.default(element.props, element.name, widgetConfig) } createCheckboxElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.checkbox') widgetConfig.label = - widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') return new checkbox_1.default(element.props, element.name, widgetConfig) } createSelectElement(element) { const widgetConfig = lodash_1.get(this._config, 'widgets.select') widgetConfig.label = - widgetConfig.label || lodash_1.get(this._config, 'widgets.label') + widgetConfig.label || + lodash_1.get(this._config, 'widgets.label.widget') return new select_1.default(element.props, element.name, widgetConfig) } createButtonElement(element) { diff --git a/dist/widgets/wrapper.js b/dist/widgets/wrapper.js index 45fa851..fba6b8a 100644 --- a/dist/widgets/wrapper.js +++ b/dist/widgets/wrapper.js @@ -1,10 +1,13 @@ 'use strict' Object.defineProperty(exports, '__esModule', { value: true }) function wrap(elements, widgetConfig) { - if (widgetConfig.wrapperOpening && widgetConfig.wrapperClosure) + if (widgetConfig.wrapper) { return ( - widgetConfig.wrapperOpening + elements + widgetConfig.wrapperClosure + widgetConfig.wrapper.opening + + elements + + widgetConfig.wrapper.closure ) + } return elements } exports.wrap = wrap diff --git a/package-lock.json b/package-lock.json index 8f06b6b..08f85c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -818,6 +818,11 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.134.tgz", "integrity": "sha512-2/O0khFUCFeDlbi7sZ7ZFRCcT812fAeOLm7Ev4KbwASkZ575TDrDcY7YyaoHdTOzKcNbfiwLYZqPmoC4wadrsw==" }, + "@types/mustache": { + "version": "0.8.32", + "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-0.8.32.tgz", + "integrity": "sha512-RTVWV485OOf4+nO2+feurk0chzHkSjkjALiejpHltyuMf/13fGymbbNNFrSKdSSUg1TIwzszXdWsVirxgqYiFA==" + }, "@types/node": { "version": "10.14.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz", @@ -5364,6 +5369,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "mustache": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", + "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==" + }, "nan": { "version": "2.13.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", diff --git a/package.json b/package.json index b922562..4ff97e4 100644 --- a/package.json +++ b/package.json @@ -74,10 +74,12 @@ "@oclif/command": "^1.5.12", "@oclif/config": "^1.12.12", "@types/lodash": "^4.14.134", + "@types/mustache": "^0.8.32", "case": "^1.6.1", "concordialang-ui-core": "^0.2.3", "cosmiconfig": "^5.2.1", "lodash": "^4.17.11", + "mustache": "^3.0.1", "normalize-diacritics": "^2.5.0", "tslib": "^1.9.3" }, diff --git a/src/html-ui-prototyper.ts b/src/html-ui-prototyper.ts index 3111166..69e10e9 100644 --- a/src/html-ui-prototyper.ts +++ b/src/html-ui-prototyper.ts @@ -3,8 +3,8 @@ import * as fs from 'fs' import { promisify } from 'util' import { format } from 'path' import { convertCase } from './utils/case-converter' +import { formatHtml } from './utils/format-html' -const prettier = require('prettier') const cosmiconfig = require('cosmiconfig') const { normalize } = require('normalize-diacritics') @@ -35,7 +35,7 @@ export default class HtmlUIPrototyper implements Prototyper { return result + widget.renderToString() }, '') - content = prettier.format(`
${content}
`, {parser: 'html', htmlWhitespaceSensitivity: 'ignore'}) + content = formatHtml(`
${content}
`) const path = format({ dir: this._outputDir, name: fileName, ext: '.html' }) await promisify(fs.writeFile)(path, content) diff --git a/src/interfaces/app-config.ts b/src/interfaces/app-config.ts index 64f90f0..83cdbb4 100644 --- a/src/interfaces/app-config.ts +++ b/src/interfaces/app-config.ts @@ -1,15 +1,27 @@ export interface AppConfig { + // TODO: add a property to config the widget properties case widgets?: { [key: string]: WidgetConfig, } } export interface WidgetConfig { - opening: string, - closure?: string, - optionOpening?: string, - optionClosure?: string, - wrapperOpening?: string, - wrapperClosure?: string, - label?: WidgetConfig + template?: string, + widget: { + opening: string, + closure?: string, + onePerValue?: boolean, + } + valueWrapper?: { + opening: string, + closure: string, + }, + wrapper?: { + opening: string, + closure: string, + } + label?: { + opening: string, + closure: string, + } } diff --git a/src/utils/format-html.ts b/src/utils/format-html.ts new file mode 100644 index 0000000..8f08182 --- /dev/null +++ b/src/utils/format-html.ts @@ -0,0 +1,5 @@ +const prettier = require('prettier') + +export function formatHtml(html: string) { + return prettier.format(html, { parser: 'html', htmlWhitespaceSensitivity: 'ignore' }) +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..c1455ac --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './case-converter' +export * from './format-html' +export * from './prop' diff --git a/src/utils/prop.ts b/src/utils/prop.ts index 1ec42fd..91fbd48 100644 --- a/src/utils/prop.ts +++ b/src/utils/prop.ts @@ -1,8 +1,6 @@ import { convertCase } from './case-converter' -export const PROPS_INJECTION_POINT = '%s' - -export function formatProperties(props: any, validProperties: string[], caseType: string = 'camel'): string { +export function formatProperties(props: any, caseType: string = 'camel'): string { const translateProp = (key: string) => { switch(key) { case 'format': return 'pattern'; @@ -10,16 +8,13 @@ export function formatProperties(props: any, validProperties: string[], caseType } } - const getFormattedProp = (key: string) => { - let value = convertCase(props[key].toString(), caseType) - return `${translateProp(key)}="${value}"` - } + const getValueOf = (key: string) => (convertCase(props[key].toString(), caseType)) - const formatValid = (result: string, prop: string) => { - return validProperties.includes(prop) - ? result + getFormattedProp(prop) + ' ' - : result + const format = (result: string, key: string) => { + const value = getValueOf(key) + const htmlProp = translateProp(key) + return result + `${htmlProp}="${value}"` + ' ' } - return Object.keys(props).reduce(formatValid, '').trimRight() + return Object.keys(props).reduce(format, '').trimRight() } diff --git a/src/widgets/button.ts b/src/widgets/button.ts index 8a93ff9..6cbdbb9 100644 --- a/src/widgets/button.ts +++ b/src/widgets/button.ts @@ -1,24 +1,29 @@ -import { Widget } from 'concordialang-ui-core' +import { pick } from 'lodash' + import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' +import { formatProperties } from '../utils/prop' -export default class Button extends Widget { - private readonly VALID_PROPERTIES = ['id', 'disabled', 'value'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name || '') +export default class Button extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) + this.props.value = this.props.value || name } - public renderToString(): string { - const buttonType = this.getType(this.props.datatype as string) - let properties = formatProperties(this.props, this.VALID_PROPERTIES) - properties = `${ buttonType } ${ properties }` - const buttonOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) - const buttonClosure = this._config.closure - return buttonOpening + this.name + buttonClosure + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'disabled'] + + props.type = this.getType(props.datatype) + props.value = props.value || this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } private getType(datatype: string): string { - return `type="${datatype || 'button'}"` + return datatype || 'button' } } diff --git a/src/widgets/checkbox.ts b/src/widgets/checkbox.ts index 4c78bca..3f5df1b 100644 --- a/src/widgets/checkbox.ts +++ b/src/widgets/checkbox.ts @@ -1,21 +1,24 @@ -import { Widget } from 'concordialang-ui-core' +import { pick } from 'lodash' + import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' -import { wrap } from './wrapper' +import { formatProperties } from '../utils/prop' -export default class Checkbox extends Widget { - private readonly VALID_PROPERTIES = ['value', 'required'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name) +export default class Checkbox extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - public renderToString(): string { - const inputType = 'type="checkbox"' - let properties = formatProperties(this.props, this.VALID_PROPERTIES) - properties = `${inputType} ${properties}` - const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) - const inputClosure = this._config.closure || '' - return wrap(inputOpening + this.name + inputClosure, this._config) + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value', 'required'] + + props.type = 'checkbox' + props.name = props.value + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } } diff --git a/src/widgets/html-widget.ts b/src/widgets/html-widget.ts new file mode 100644 index 0000000..57c7c00 --- /dev/null +++ b/src/widgets/html-widget.ts @@ -0,0 +1,94 @@ +import { Widget } from 'concordialang-ui-core' +import { get } from 'lodash' +import * as Mustache from 'mustache' + +import { WidgetConfig } from '../interfaces/app-config' +import { formatProperties } from '../utils/prop' + +import { createLabel } from './label' +import { wrap } from './wrapper' + +export default abstract class HtmlWidget extends Widget { + constructor(props: any, name: string, private _config: WidgetConfig) { + super(props, name) + } + + public renderToString(): string { + let main: string + + if (Array.isArray(this.props.value)) { + main = this._config.widget.onePerValue + ? this.renderOneWidgetPerValue(this.props.value as string []) + : this.renderWidgetWithMultipleValues(this.props.value as string[]) + } else { + main = this.renderWidgetWithSingleValue() + } + + const widgetId = this.props.id ? this.props.id.toString() : undefined + const label: string = createLabel(this.name, widgetId, this._config) + return wrap(label + main, this._config) + } + + protected abstract getFormattedProps(props: any): string + + private renderWidgetWithSingleValue(): string { + const props = this.getFormattedProps(this.props) + const config = { + widget: { ...this._config.widget }, + // Widgets like button may include {{value}} in the template + value: this.props.value + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + const template = this._config.template || '{{&widget.opening}}{{&widget.closure}}' + return Mustache.render(template, config) + } + + private renderOneWidgetPerValue(values: string[]): string { + // When rendering one widget per value, we must delete the "id" property + // to avoid multiple widgets with the same id. + delete this.props.id + + const widgets: string[] = [] + + for (const value of values) { + const props = this.getFormattedProps({ ...this.props, value }) + const config = { + widget: { ...this._config.widget }, + valueWrapper: { ...this._config.valueWrapper }, + value + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + + const template = this._config.template || '{{&widget.opening}}{{&widget.closure}}{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgets.push(Mustache.render(template, config)) + } + + return widgets.join(' ') + } + + private renderWidgetWithMultipleValues(values: string[]): string { + const widgetValues: string[] = [] + for (const value of values) { + const props = formatProperties({ value }) + const config = { + valueWrapper: this._config.valueWrapper ? { ...this._config.valueWrapper } : { opening: '', closure: '' }, + value + } + + config.valueWrapper.opening = Mustache.render(config.valueWrapper.opening, { props }) + + const template = '{{&valueWrapper.opening}}{{value}}{{&valueWrapper.closure}}' + widgetValues.push(Mustache.render(template, config)) + } + + delete this.props.value + const props = this.getFormattedProps(this.props) + const config = { + widget: { ...this._config.widget }, + values: widgetValues.join(' ') + } + config.widget.opening = Mustache.render(config.widget.opening, { props }) + const template = '{{&widget.opening}}{{&values}}{{&widget.closure}}' + return Mustache.render(template, config) + } +} diff --git a/src/widgets/input.ts b/src/widgets/input.ts index adb938e..d831df3 100644 --- a/src/widgets/input.ts +++ b/src/widgets/input.ts @@ -1,10 +1,9 @@ -import { Widget } from 'concordialang-ui-core' -import { get } from 'lodash'; +import { pick } from 'lodash' import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' -import { createLabel } from './label' -import { wrap } from './wrapper' +import { formatProperties } from '../utils/prop' + +import HtmlWidget from './html-widget' const enum DataTypes { STRING = 'string', @@ -15,33 +14,34 @@ const enum DataTypes { DATETIME = 'datetime' } -export default class Input extends Widget { - private readonly VALID_PROPERTIES = ['id', 'editable', 'minlength', 'maxlength', 'required', 'format'] - - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name) +export default class Input extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - public renderToString(): string { - const inputType = this.getType(this.props.datatype as string) - const properties = formatProperties(this.props, this.VALID_PROPERTIES) - const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, `${ inputType } ${ properties }`) - const inputClosure = this._config.closure || '' - const label = createLabel(this.name, this.props.id.toString(), this._config) - return wrap(label + inputOpening + inputClosure, this._config) + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'type', 'name', 'editable', 'minlength', 'maxlength', 'required', 'format'] + + props.type = this.getType(props.datatype) + props.name = this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } private getType(datatype: string): string { let typeProperty switch (datatype) { - case DataTypes.INTEGER: - case DataTypes.DOUBLE: typeProperty = 'number'; break - case DataTypes.TIME: typeProperty = 'time'; break - case DataTypes.DATETIME: typeProperty = 'datetime-local'; break - default: typeProperty = 'text' + case DataTypes.INTEGER: + case DataTypes.DOUBLE: typeProperty = 'number'; break + case DataTypes.TIME: typeProperty = 'time'; break + case DataTypes.DATETIME: typeProperty = 'datetime-local'; break + default: typeProperty = 'text' } - return `type="${typeProperty}"` + return typeProperty } } diff --git a/src/widgets/label.ts b/src/widgets/label.ts index a75a5e2..798ffa7 100644 --- a/src/widgets/label.ts +++ b/src/widgets/label.ts @@ -1,13 +1,14 @@ +import * as Mustache from 'mustache' + import { WidgetConfig } from '../interfaces/app-config' -import { PROPS_INJECTION_POINT } from '../utils/prop' +import { formatHtml } from '../utils' -export function createLabel(widgetName: string, widgetId: string, widgetConfig: WidgetConfig): string { +export function createLabel(widgetName: string, widgetId: string | undefined, widgetConfig: WidgetConfig): string { if (!widgetConfig.label) return '' const idPattern = /^(#|~|\d|\w).*/ - const labelFor = widgetId.match(idPattern) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' - const labelOpening = widgetConfig.label.opening.replace(PROPS_INJECTION_POINT, labelFor) - const labelClosure = widgetConfig.label.closure + const labelFor = widgetId && widgetId.match(idPattern) ? `for="${widgetId.replace(/^#|~/ , '')}"` : '' + widgetConfig.label.opening = Mustache.render(widgetConfig.label.opening, { props: labelFor }) - return labelOpening + widgetName + labelClosure + return formatHtml(widgetConfig.label.opening + widgetName + widgetConfig.label.closure) } diff --git a/src/widgets/radio.ts b/src/widgets/radio.ts index 0044ec3..735e3fd 100644 --- a/src/widgets/radio.ts +++ b/src/widgets/radio.ts @@ -1,37 +1,24 @@ -import { Widget } from 'concordialang-ui-core' +import { pick } from 'lodash' + import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' -import { createLabel } from './label' -import { wrap } from './wrapper' +import { formatProperties } from '../utils/prop' -export default class Radio extends Widget { - private readonly VALID_PROPERTIES = [ 'value' ] +import HtmlWidget from './html-widget' - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name) +export default class Radio extends HtmlWidget { + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - public renderToString(): string { - const inputType = 'type="radio"' - const label = createLabel(this.name, '', this._config) - let inputs: String[] = [] + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['type', 'name', 'value'] - for (let value of this.props.value as Array) { - // TODO: o que fazer no formatProperties em relação ao value? - // provavelmente terei que instalar o pacote "case" - // para ter 'value="algumaCoisa"', quando value for "Alguma Coisa" - // - // TODO: adicionar propriedades 'id' e 'nome' + props.type = 'radio' + props.name = this.name - const props = Object.assign({}, this.props, { value }) - let properties = formatProperties(props, this.VALID_PROPERTIES) - properties = `${inputType} ${properties}` - const inputOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) - const inputClosure = this._config.closure || '' - inputs.push(inputOpening + value + inputClosure) - } + const filteredProps = pick(props, VALID_PROPERTIES) - return wrap(label + inputs.join(''), this._config) + return formatProperties(filteredProps) } } - diff --git a/src/widgets/select.ts b/src/widgets/select.ts index 856a86f..113dd87 100644 --- a/src/widgets/select.ts +++ b/src/widgets/select.ts @@ -1,38 +1,25 @@ -import { Widget } from 'concordialang-ui-core' +import { pick } from 'lodash' + import { WidgetConfig } from '../interfaces/app-config' -import { formatProperties, PROPS_INJECTION_POINT } from '../utils/prop' -import { createLabel } from './label' -import { wrap } from './wrapper' +import { formatProperties } from '../utils/prop' -export default class Select extends Widget { - private readonly SELECT_VALID_PROPERTIES = ['id', 'required'] - private readonly OPTION_VALID_PROPERTIES = ['value'] +import HtmlWidget from './html-widget' - constructor(props: any, name: string, private _config: WidgetConfig) { - super(props, name) - } +export default class Select extends HtmlWidget { + private readonly VALID_PROPERTIES = [] - public renderToString(): string { - const properties = formatProperties(this.props, this.SELECT_VALID_PROPERTIES) - const selectOpening = this._config.opening.replace(PROPS_INJECTION_POINT, properties) - const selectClosure = this._config.closure - const options = this.getOptions() - const select = selectOpening + options + selectClosure - const label = createLabel(this.name, this.props.id.toString(), this._config) - return wrap(label + select, this._config) + constructor(props: any, name: string, config: WidgetConfig) { + super(props, name, config) } - private getOptions(): string { - if (!this._config.optionOpening) return '' - - let options: string[] = [] - for (let value of this.props.value as Array) { - const optionProps = { value } - const properties = formatProperties(optionProps, this.OPTION_VALID_PROPERTIES) - const optionOpening = this._config.optionOpening.replace(PROPS_INJECTION_POINT, properties) - const optionClosure = this._config.optionClosure - options.push(optionOpening + value + optionClosure) - } - return options.join('') + protected getFormattedProps(props: any): string { + // Defines the properties that will be injected in the widget and its order. + const VALID_PROPERTIES = ['id', 'name', 'required'] + + props.name = this.name + + const filteredProps = pick(props, VALID_PROPERTIES) + + return formatProperties(filteredProps) } } diff --git a/src/widgets/widget-factory.ts b/src/widgets/widget-factory.ts index e6dca4c..5e2bd0e 100644 --- a/src/widgets/widget-factory.ts +++ b/src/widgets/widget-factory.ts @@ -1,12 +1,13 @@ -import {UiElement, Widget} from 'concordialang-ui-core' -import { get } from 'lodash'; +import { UiElement, Widget } from 'concordialang-ui-core' +import { get } from 'lodash' + +import { AppConfig, WidgetConfig } from '../interfaces/app-config' import Button from './button' -import Input from './input' import Checkbox from './checkbox' +import Input from './input' import Radio from './radio' import Select from './select' -import { AppConfig, WidgetConfig } from '../interfaces/app-config' const enum Widgets { BUTTON = 'button', @@ -32,25 +33,25 @@ export default class WidgetFactory { private createInputElement(element: UiElement): Input { const widgetConfig: WidgetConfig = get(this._config, 'widgets.input') - widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') return new Input(element.props, element.name, widgetConfig) } private createRadioElement(element: UiElement): Radio { const widgetConfig: WidgetConfig = get(this._config, 'widgets.radio') - widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') return new Radio(element.props, element.name, widgetConfig) } private createCheckboxElement(element: UiElement): Checkbox { const widgetConfig: WidgetConfig = get(this._config, 'widgets.checkbox') - widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') return new Checkbox(element.props, element.name, widgetConfig) } private createSelectElement(element: UiElement): Select { const widgetConfig: WidgetConfig = get(this._config, 'widgets.select') - widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label') + widgetConfig.label = widgetConfig.label || get(this._config, 'widgets.label.widget') return new Select(element.props, element.name, widgetConfig) } diff --git a/src/widgets/wrapper.ts b/src/widgets/wrapper.ts index 5925db1..54c6a75 100644 --- a/src/widgets/wrapper.ts +++ b/src/widgets/wrapper.ts @@ -1,7 +1,8 @@ import { WidgetConfig } from '../interfaces/app-config' export function wrap(elements: string, widgetConfig: WidgetConfig): string { - if (widgetConfig.wrapperOpening && widgetConfig.wrapperClosure) - return widgetConfig.wrapperOpening + elements + widgetConfig.wrapperClosure + if (widgetConfig.wrapper) { + return widgetConfig.wrapper.opening + elements + widgetConfig.wrapper.closure + } return elements } diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index 7a3dea5..1d409da 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -1,4 +1,5 @@ import { fs as memfs, vol } from 'memfs' + import Generate from '../../src/commands/generate' import { completeAppConfig } from '../fixtures/app-config' import { featureWithName } from '../fixtures/features' @@ -8,23 +9,23 @@ jest.mock('util') describe('Generate', () => { const CURRENT_DIR: string = process.cwd() - const OUTPUT_DIR: string = 'outputDir' + const OUTPUT_DIR = 'outputDir' - const mockFiles = (files) => { vol.fromJSON(files, CURRENT_DIR) } + const mockFiles = files => { vol.fromJSON(files, CURRENT_DIR) } afterAll(() => { require('fs').writeFile.mockRestore() }) describe('with a complete app config', () => { - let spy + let consoleOutputSpy beforeAll(async () => { vol.reset() mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) vol.mkdirSync(OUTPUT_DIR) - spy = jest.spyOn(process.stdout, 'write'); + consoleOutputSpy = jest.spyOn(process.stdout, 'write') const features: string = JSON.stringify(featureWithName('Login de usuário')) await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) @@ -35,25 +36,25 @@ describe('Generate', () => { }) it('should list the generated file in the console', () => { - expect(spy).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html\n`) + expect(consoleOutputSpy).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html\n`) }) }) describe('without features', () => { - let spy + let consoleOutputSpy beforeAll(async () => { vol.reset() mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) vol.mkdirSync(OUTPUT_DIR) - spy = jest.spyOn(process.stdout, 'write'); - const features: string = '{ "features": [] }' + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + const features = '{ "features": [] }' await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) it('should show an error message', async () => { - expect(spy).toBeCalledWith(expect.stringContaining("No features found")) + expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('No features found')) }) it('should not write any file', () => { @@ -62,19 +63,19 @@ describe('Generate', () => { }) describe('without the outputDir flag', () => { - let spy + let consoleOutputSpy beforeAll(async () => { vol.reset() mockFiles({ 'concordialang-ui-html.json': completeAppConfig }) - spy = jest.spyOn(process.stdout, 'write'); + consoleOutputSpy = jest.spyOn(process.stdout, 'write') const features: string = JSON.stringify(featureWithName('Login de usuário')) await Generate.run(['--features', features]) }) it('should show an error message', async () => { - expect(spy).toBeCalledWith(expect.stringContaining("Missing required flag")) + expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('Missing required flag')) }) it('should not write any file', () => { @@ -83,18 +84,18 @@ describe('Generate', () => { }) describe('without an app config', () => { - let spy + let consoleOutputSpy beforeAll(async () => { vol.reset() - spy = jest.spyOn(process.stdout, 'write'); - const features: string = '{"features":[{"name":"Login de usuário","position":2,"uiElements":[{"name":"Nome de Usuário","widget":"textbox","position":22,"props":{"id":"nome_usuario"}},{"name":"Senha","widget":"textbox","position":26,"props":{"id":"senha","required":true}},{"name":"Entrar","widget":"button","position":31,"props":{}}]}]}' + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + const features = '{"features":[{"name":"Login de usuário","position":2,"uiElements":[{"name":"Nome de Usuário","widget":"textbox","position":22,"props":{"id":"nome_usuario"}},{"name":"Senha","widget":"textbox","position":26,"props":{"id":"senha","required":true}},{"name":"Entrar","widget":"button","position":31,"props":{}}]}]}' await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) it('should show an error message', async () => { - expect(spy).toBeCalledWith(expect.stringContaining("Config file not found")) + expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('Config file not found')) }) it('should not write any file', () => { diff --git a/test/fixtures/app-config.ts b/test/fixtures/app-config.ts index e93a3bd..64f1a97 100644 --- a/test/fixtures/app-config.ts +++ b/test/fixtures/app-config.ts @@ -1,17 +1,26 @@ -export const completeAppConfig: string = JSON.stringify({ +export const completeAppConfigObject = { widgets: { input: { - opening: '', - wrapperOpening: '
', - wrapperClosure: '
' + widget: { + opening: '' + }, + wrapper: { + opening: '
', + closure: '
' + } }, label: { - opening: '' + widget: { + opening: '' + } }, button: { - opening: '' + widget: { + opening: '' + } } } -}) +} +export const completeAppConfig: string = JSON.stringify(completeAppConfigObject) diff --git a/test/utils/format-properties.spec.ts b/test/utils/format-properties.spec.ts index 17bc7ae..9c269a8 100644 --- a/test/utils/format-properties.spec.ts +++ b/test/utils/format-properties.spec.ts @@ -1,18 +1,13 @@ import { formatProperties } from '../../src/utils/prop' describe('formatProperties', () => { - describe('when there is an invalid property', () => { - const props = { - id: 'id', - name: 'name', - required: true, - foo: 'bar' - } + const props = { + id: 'id', + name: 'name', + required: true + } - const validProperties = ['id', 'name', 'required'] - - it('produces a string with the valid properties only', () => { - expect(formatProperties(props, validProperties)).toEqual('id="id" name="name" required="true"') - }) + it('produces a string with the properties', () => { + expect(formatProperties(props)).toEqual('id="id" name="name" required="true"') }) }) diff --git a/test/widgets/button.spec.ts b/test/widgets/button.spec.ts index c46a90f..5e18367 100644 --- a/test/widgets/button.spec.ts +++ b/test/widgets/button.spec.ts @@ -1,29 +1,36 @@ -import { UiElement} from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' -import Button from '../../src/widgets/button' - +import { UiElement } from 'concordialang-ui-core' +import { WidgetConfig } from '../../src/interfaces/app-config' +import Button from '../../src/widgets/button' describe('Button', () => { - describe('renderToString', () => { - const uiElement: UiElement = { - name: 'Save', - widget: 'button', - position: 7, - props: { - id: 'save' + describe('renderToString', () => { + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Save', + widget: 'button', + position: 7, + props: { + id: 'save' + } } - } - const widgetConfig: WidgetConfig = { - opening: '' - } + widgetConfig = { + template: '{{&widget.opening}}{{value}}{{&widget.closure}}', + widget: { + opening: '' + } + } + }) - it('produces html from a button element', async () => { + it('produces html from a button element', async () => { const buttonWidget: Button = new Button(uiElement.props, uiElement.name, widgetConfig) - const result = buttonWidget.renderToString() - expect(result).toEqual(``) - }) - }) + const result = buttonWidget.renderToString() + expect(result).toEqual('') + }) + }) }) diff --git a/test/widgets/checkbox.spec.ts b/test/widgets/checkbox.spec.ts index 5bea6c6..e28b75e 100644 --- a/test/widgets/checkbox.spec.ts +++ b/test/widgets/checkbox.spec.ts @@ -1,38 +1,60 @@ import { UiElement } from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' + +import { WidgetConfig } from '../../src/interfaces/app-config' import Checkbox from '../../src/widgets/checkbox' describe('Checkbox', () => { - describe('renderToString', () => { - const uiElement: UiElement = { - name: 'Web Developer', - widget: 'checkbox', - position: 16, - props: { - value: 'web_developer' + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Web Developer Skills', + widget: 'checkbox', + position: 16, + props: { + value: ['html', 'css', 'javascript'] + } } - } - const widgetConfig: WidgetConfig = { - opening: '', - wrapperOpening: '
', - wrapperClosure: '
', - label: { - opening: '' + widgetConfig = { + widget: { + opening: '', + onePerValue: true, + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } } - } + }) + + it('produces html from an input element with name', async () => { + const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) + const result = inputWidget.renderToString() + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) + }) - it('produces html from an input element with name', async () => { + it('produces a label for the input element', async () => { const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Web Developer')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) it('produces a wrapper for the input element', () => { const inputWidget: Checkbox = new Checkbox(uiElement.props, uiElement.name, widgetConfig) - const result = inputWidget.renderToString() + const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) }) diff --git a/test/widgets/input.spec.ts b/test/widgets/input.spec.ts index c68fb18..040ab7b 100644 --- a/test/widgets/input.spec.ts +++ b/test/widgets/input.spec.ts @@ -1,55 +1,63 @@ import { UiElement } from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' + +import { WidgetConfig } from '../../src/interfaces/app-config' import Input from '../../src/widgets/input' describe('Input', () => { - describe('renderToString', () => { - const uiElement: UiElement = { - name: 'Username', - widget: 'textbox', - position: 16, - props: { - id: 'username', - required: true, - maxlength: 20, - minlength: 10 + describe('renderToString', () => { + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Username', + widget: 'textbox', + position: 16, + props: { + id: 'username', + required: true, + maxlength: 20, + minlength: 10 + } } - } - - const widgetConfig: WidgetConfig = { - opening: '', - wrapperOpening: '
', - wrapperClosure: '
', - label: { - opening: '' + + widgetConfig = { + widget: { + opening: '', + }, + wrapper: { + opening: '
', + closure: '
', + }, + label: { + opening: '' + } } - } + }) - it('produces html from an input element with name', async () => { + it('produces html from an input element with name', async () => { const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) - }) + expect(result).toEqual(expect.stringContaining('')) + }) it('produces a label for the input element', async () => { const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) it('produces a wrapper for the input element', () => { const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) - const result = inputWidget.renderToString() + const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) describe('when the label is not defined', () => { - const widgetConfig: WidgetConfig = { - opening: '', - wrapperOpening: '
', - wrapperClosure: '
' - } + beforeEach(() => { + widgetConfig.label = undefined + }) it('does not produce a label for the input element', async () => { const inputWidget: Input = new Input(uiElement.props, uiElement.name, widgetConfig) @@ -57,5 +65,5 @@ describe('Input', () => { expect(result).not.toEqual(expect.stringContaining('label')) }) }) - }) + }) }) diff --git a/test/widgets/prop.spec.ts b/test/widgets/prop.spec.ts deleted file mode 100644 index d0e748a..0000000 --- a/test/widgets/prop.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { formatProperties } from '../../src/utils/prop' - -describe('formatProperties', () => { - it('creates a string with the valid properties', () => { - const props = { id: 'id', name: 'name', value: 'value' } - const validProperties = ['id', 'name'] - expect(formatProperties(props, validProperties)).toBe('id="id" name="name"') - }) -}) diff --git a/test/widgets/radio.spec.ts b/test/widgets/radio.spec.ts index 1e5c73d..63378b5 100644 --- a/test/widgets/radio.spec.ts +++ b/test/widgets/radio.spec.ts @@ -1,40 +1,55 @@ import { UiElement } from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' + +import { WidgetConfig } from '../../src/interfaces/app-config' import Radio from '../../src/widgets/radio' describe('Radio', () => { describe('renderToString', () => { - const uiElement: UiElement = { - name: 'Gender', - widget: 'radio', - position: 7, - props: { - id: 'gender', - value: ['Male', 'Female'] + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Gender', + widget: 'radio', + position: 7, + props: { + id: 'gender', + value: ['Male', 'Female'] + } } - } - const widgetConfig: WidgetConfig = { - opening: '', - wrapperOpening: '
', - wrapperClosure: '
', - label: { - opening: '' + widgetConfig = { + widget: { + opening: '', + onePerValue: true, + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } } - } + }) - it('produces html from an radio element with name', async () => { + it('produces html from an radio element with name', async () => { const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('Male')) - expect(result).toEqual(expect.stringContaining('Female')) - }) + expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) + }) it('produces a label for the input element', async () => { const inputWidget: Radio = new Radio(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) it('produces a wrapper for the input element', () => { @@ -44,4 +59,3 @@ describe('Radio', () => { }) }) }) - diff --git a/test/widgets/select.spec.ts b/test/widgets/select.spec.ts index eca3546..c2a614d 100644 --- a/test/widgets/select.spec.ts +++ b/test/widgets/select.spec.ts @@ -1,38 +1,50 @@ import { UiElement } from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' + +import { WidgetConfig } from '../../src/interfaces/app-config' import Select from '../../src/widgets/select' describe('Select', () => { describe('renderToString', () => { - const uiElement: UiElement = { - name: 'Gender', - widget: 'select', - position: 7, - props: { - id: 'gender', - value: ['Male', 'Female'] + let uiElement: UiElement + let widgetConfig: WidgetConfig + + beforeEach(() => { + uiElement = { + name: 'Gender', + widget: 'select', + position: 7, + props: { + id: 'gender', + value: ['Male', 'Female'] + } } - } - - const widgetConfig: WidgetConfig = { - opening: '', - optionOpening: '', - wrapperOpening: '
', - wrapperClosure: '
', - label: { - opening: '' + + widgetConfig = { + widget: { + opening: '', + }, + wrapper: { + opening: '
', + closure: '
', + }, + valueWrapper: { + opening: '', + }, + label: { + opening: '' + } } - } + }) - it('produces html from an select element with name', async () => { + it('produces html from an select element with name', async () => { const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) expect(result).toEqual(expect.stringContaining('')) - }) + }) it('produces the options for the select element', async () => { const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) @@ -44,12 +56,12 @@ describe('Select', () => { it('produces a label for the select element', async () => { const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) const result = inputWidget.renderToString() - expect(result).toEqual(expect.stringContaining('')) + expect(result).toEqual(expect.stringContaining('')) }) it('produces a wrapper for the input element', () => { const inputWidget: Select = new Select(uiElement.props, uiElement.name, widgetConfig) - const result = inputWidget.renderToString() + const result = inputWidget.renderToString() expect(result).toEqual(expect.stringMatching(/^
(.|\s)*<\/div>$/)) }) }) diff --git a/test/widgets/widget-factory.spec.ts b/test/widgets/widget-factory.spec.ts index 74167ed..6f9ee03 100644 --- a/test/widgets/widget-factory.spec.ts +++ b/test/widgets/widget-factory.spec.ts @@ -1,69 +1,55 @@ import { UiElement } from 'concordialang-ui-core' -import { AppConfig, WidgetConfig } from '../../src/interfaces/app-config' -import WidgetFactory from '../../src/widgets/widget-factory' -import Button from '../../src/widgets/button'; -import Input from '../../src/widgets/input'; - +import Button from '../../src/widgets/button' +import Input from '../../src/widgets/input' +import WidgetFactory from '../../src/widgets/widget-factory' +import { completeAppConfigObject } from '../fixtures/app-config' describe('WidgetFactory', () => { - const appConfig: AppConfig = { - widgets: { - input: { - opening: '', - label: { - opening: '' + let widgetFactory: WidgetFactory = new WidgetFactory(completeAppConfigObject) + + describe('create', () => { + it('create button with valid properties', () => { + const buttonUiElement: UiElement = { + name: 'OK', + widget: 'button', + position: 30, + props: {} + } + const { button: buttonConfig } = completeAppConfigObject.widgets + const buttonWidget = new Button(buttonUiElement.props, buttonUiElement.name, buttonConfig) + + expect(widgetFactory.create(buttonUiElement)).toEqual(buttonWidget) + }) + + it('create input with valid properties', async () => { + const inputUiElement: UiElement = { + name: 'Username', + widget: 'textbox', + position: 16, + props: { + required: true, + maxlength: 20, + minlength: 10 } } - } - } - - let widgetFactory: WidgetFactory = new WidgetFactory(appConfig) - - describe('create', () => { - it('create button with valid properties', () => { - const buttonUiElement: UiElement = { - name: 'OK', - widget: 'button', - position: 30, - props: {} - } + const { input: inputConfig } = completeAppConfigObject.widgets + const inputWidget = new Input(inputUiElement.props, inputUiElement.name, inputConfig) - const buttonWidget = new Button(buttonUiElement.props, buttonUiElement.name) + expect(widgetFactory.create(inputUiElement)).toEqual(inputWidget) + }) - expect(widgetFactory.create(buttonUiElement)).toEqual(buttonWidget) - }) - - it('create input with valid properties', async () => { - const inputUiElement: UiElement = { - name: 'Username', - widget: 'textbox', - position: 16, - props: { - required: true, - maxlength: 20, - minlength: 10 - } - } - const widgetConfig: WidgetConfig = appConfig.widgets.input - - const inputWidget = new Input(inputUiElement.props, inputUiElement.name, widgetConfig) - - expect(widgetFactory.create(inputUiElement)).toEqual(inputWidget) - }) - - it('throw invalid widget error', async () => { - const inputUiElement: UiElement = { - widget: 'invalid', + it('throw invalid widget error', async () => { + const inputUiElement: UiElement = { + widget: 'invalid', name: '', - position: 16, - props: {} - } + position: 16, + props: {} + } - expect(() => { - widgetFactory.create(inputUiElement) - }).toThrow(Error); - }) - }) + expect(() => { + widgetFactory.create(inputUiElement) + }).toThrow(Error) + }) + }) }) diff --git a/tslint.json b/tslint.json index cc0834d..d364f8f 100644 --- a/tslint.json +++ b/tslint.json @@ -1,3 +1,10 @@ { - "ter-indent": [true, 4] + "extends": "@oclif/tslint", + "rules": { + "indent": { + "options": ["tabs"] + }, + "ter-indent": [true, "tab"], + "object-curly-spacing": [true, "always"] + } } From cb25f1eeef48ea9f2f31a48221e558cd7dce38db Mon Sep 17 00:00:00 2001 From: Willian Date: Mon, 19 Aug 2019 23:05:04 -0300 Subject: [PATCH 9/9] Implement a class to print formatted messages --- dist/commands/generate.js | 6 +++-- dist/utils/printer.d.ts | 4 +++ dist/utils/printer.js | 21 ++++++++++++++++ package-lock.json | 19 ++++++++++++-- package.json | 1 + src/commands/generate.ts | 23 +++++++++-------- src/utils/printer.ts | 22 ++++++++++++++++ test/commands/generate.spec.ts | 46 +++++++++++++++------------------- test/utils/printer.spec.ts | 37 +++++++++++++++++++++++++++ 9 files changed, 138 insertions(+), 41 deletions(-) create mode 100644 dist/utils/printer.d.ts create mode 100644 dist/utils/printer.js create mode 100644 src/utils/printer.ts create mode 100644 test/utils/printer.spec.ts diff --git a/dist/commands/generate.js b/dist/commands/generate.js index 99ba489..fd0a18d 100644 --- a/dist/commands/generate.js +++ b/dist/commands/generate.js @@ -4,9 +4,11 @@ const tslib_1 = require('tslib') const command_1 = require('@oclif/command') const fs = require('fs') const html_ui_prototyper_1 = require('../html-ui-prototyper') +const printer_1 = require('../utils/printer') class Generate extends command_1.Command { run() { return tslib_1.__awaiter(this, void 0, void 0, function*() { + const printer = new printer_1.default() try { const { flags } = this.parse(Generate) if (!flags.features) throw new Error('Missing flag --features') @@ -18,9 +20,9 @@ class Generate extends command_1.Command { flags.outputDir ) const result = yield generator.generate(processResult.features) - this.log(result.join('\n')) + printer.printGeneratedFiles(result) } catch (e) { - this.log(e.message) + printer.printErrorMessage(e.message) } }) } diff --git a/dist/utils/printer.d.ts b/dist/utils/printer.d.ts new file mode 100644 index 0000000..43df5df --- /dev/null +++ b/dist/utils/printer.d.ts @@ -0,0 +1,4 @@ +export default class Printer { + printGeneratedFiles(files: string[]): void; + printErrorMessage(message: string): void; +} diff --git a/dist/utils/printer.js b/dist/utils/printer.js new file mode 100644 index 0000000..c45877d --- /dev/null +++ b/dist/utils/printer.js @@ -0,0 +1,21 @@ +'use strict' +Object.defineProperty(exports, '__esModule', { value: true }) +const Table = require('cli-table3') +const colors = require('colors') +/* tslint:disable:no-console */ +class Printer { + printGeneratedFiles(files) { + const table = new Table({ + head: [colors.green('#'), colors.green('Generated files')], + }) + for (let i = 0; i < files.length; i++) { + const counter = i + 1 + table.push([counter, files[i]]) + } + console.log(table.toString()) + } + printErrorMessage(message) { + console.log(colors.red(message)) + } +} +exports.default = Printer diff --git a/package-lock.json b/package-lock.json index 08f85c9..c4c5148 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1624,6 +1624,16 @@ "restore-cursor": "^2.0.0" } }, + "cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + } + }, "cli-truncate": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", @@ -1832,6 +1842,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==", + "optional": true + }, "combined-stream": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", @@ -5534,8 +5550,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", diff --git a/package.json b/package.json index 4ff97e4..4e8805f 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@types/lodash": "^4.14.134", "@types/mustache": "^0.8.32", "case": "^1.6.1", + "cli-table3": "^0.5.1", "concordialang-ui-core": "^0.2.3", "cosmiconfig": "^5.2.1", "lodash": "^4.17.11", diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 54993ba..5bff607 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -1,32 +1,33 @@ -import {Command, flags} from '@oclif/command' -import {ProcessResult, Feature} from 'concordialang-ui-core' +import { Command, flags } from '@oclif/command' +import { ProcessResult } from 'concordialang-ui-core' import * as fs from 'fs' import HtmlUIPrototyper from '../html-ui-prototyper' +import Printer from '../utils/printer' export default class Generate extends Command { - static description = 'Generate html files' static flags = { - help: flags.help({char: 'h'}), - features: flags.string({description: 'processed features from ast', required: true}), - outputDir: flags.string({description: 'location where output files will be saved', required: true}) + help: flags.help({ char: 'h' }), + features: flags.string({ description: 'processed features from ast', required: true }), + outputDir: flags.string({ description: 'location where output files will be saved', required: true }) } async run() { + const printer: Printer = new Printer() try { - const {flags} = this.parse(Generate) + const { flags } = this.parse(Generate) if (!flags.features) throw new Error('Missing flag --features') const processResult: ProcessResult = JSON.parse(flags.features) as ProcessResult if (processResult.features.length === 0) throw new Error('No features found') const generator = new HtmlUIPrototyper(fs, flags.outputDir) - const result = await generator.generate(processResult.features) - this.log(result.join('\n')) - } catch(e) { - this.log(e.message) + const result: string[] = await generator.generate(processResult.features) + printer.printGeneratedFiles(result) + } catch (e) { + printer.printErrorMessage(e.message) } } } diff --git a/src/utils/printer.ts b/src/utils/printer.ts new file mode 100644 index 0000000..f538081 --- /dev/null +++ b/src/utils/printer.ts @@ -0,0 +1,22 @@ +const Table = require('cli-table3') +const colors = require('colors') + +/* tslint:disable:no-console */ +export default class Printer { + public printGeneratedFiles(files: string[]) { + const table = new Table({ + head: [colors.green('#'), colors.green('Generated files')] + }) + + for (let i = 0; i < files.length; i++) { + const counter = i + 1 + table.push([counter, files[i]]) + } + + console.log(table.toString()) + } + + public printErrorMessage(message: string) { + console.log(colors.red(message)) + } +} diff --git a/test/commands/generate.spec.ts b/test/commands/generate.spec.ts index 1d409da..3e926c4 100644 --- a/test/commands/generate.spec.ts +++ b/test/commands/generate.spec.ts @@ -1,11 +1,13 @@ import { fs as memfs, vol } from 'memfs' import Generate from '../../src/commands/generate' +import Printer from '../../src/utils/printer' import { completeAppConfig } from '../fixtures/app-config' import { featureWithName } from '../fixtures/features' jest.mock('fs') jest.mock('util') +jest.mock('../../src/utils/printer') describe('Generate', () => { const CURRENT_DIR: string = process.cwd() @@ -13,19 +15,20 @@ describe('Generate', () => { const mockFiles = files => { vol.fromJSON(files, CURRENT_DIR) } + beforeEach(() => { + (Printer as jest.Mock).mockClear() + vol.reset() + }) + afterAll(() => { require('fs').writeFile.mockRestore() }) describe('with a complete app config', () => { - let consoleOutputSpy - - beforeAll(async () => { - vol.reset() + beforeEach(async () => { mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) vol.mkdirSync(OUTPUT_DIR) - consoleOutputSpy = jest.spyOn(process.stdout, 'write') const features: string = JSON.stringify(featureWithName('Login de usuário')) await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) @@ -36,25 +39,23 @@ describe('Generate', () => { }) it('should list the generated file in the console', () => { - expect(consoleOutputSpy).toBeCalledWith(`${OUTPUT_DIR}/login_de_usuario.html\n`) + const printGeneratedeFiles = (Printer as jest.Mock).mock.instances[0].printGeneratedFiles + expect(printGeneratedeFiles).toBeCalledWith(['outputDir/login_de_usuario.html']) }) }) describe('without features', () => { - let consoleOutputSpy - - beforeAll(async () => { - vol.reset() + beforeEach(async () => { mockFiles({ 'concordialang-ui-html.json': completeAppConfig, }) vol.mkdirSync(OUTPUT_DIR) - consoleOutputSpy = jest.spyOn(process.stdout, 'write') const features = '{ "features": [] }' await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) it('should show an error message', async () => { - expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('No features found')) + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('No features found')) }) it('should not write any file', () => { @@ -63,19 +64,16 @@ describe('Generate', () => { }) describe('without the outputDir flag', () => { - let consoleOutputSpy - - beforeAll(async () => { - vol.reset() + beforeEach(async () => { mockFiles({ 'concordialang-ui-html.json': completeAppConfig }) - consoleOutputSpy = jest.spyOn(process.stdout, 'write') const features: string = JSON.stringify(featureWithName('Login de usuário')) await Generate.run(['--features', features]) }) it('should show an error message', async () => { - expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('Missing required flag')) + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('Missing required flag')) }) it('should not write any file', () => { @@ -84,18 +82,14 @@ describe('Generate', () => { }) describe('without an app config', () => { - let consoleOutputSpy - - beforeAll(async () => { - vol.reset() - - consoleOutputSpy = jest.spyOn(process.stdout, 'write') - const features = '{"features":[{"name":"Login de usuário","position":2,"uiElements":[{"name":"Nome de Usuário","widget":"textbox","position":22,"props":{"id":"nome_usuario"}},{"name":"Senha","widget":"textbox","position":26,"props":{"id":"senha","required":true}},{"name":"Entrar","widget":"button","position":31,"props":{}}]}]}' + beforeEach(async () => { + const features: string = JSON.stringify(featureWithName('Login de usuário')) await Generate.run(['--features', features, '--outputDir', OUTPUT_DIR]) }) it('should show an error message', async () => { - expect(consoleOutputSpy).toBeCalledWith(expect.stringContaining('Config file not found')) + const printErrorMessage = (Printer as jest.Mock).mock.instances[0].printErrorMessage + expect(printErrorMessage).toBeCalledWith(expect.stringContaining('Config file not found')) }) it('should not write any file', () => { diff --git a/test/utils/printer.spec.ts b/test/utils/printer.spec.ts new file mode 100644 index 0000000..1e23600 --- /dev/null +++ b/test/utils/printer.spec.ts @@ -0,0 +1,37 @@ +import Printer from '../../src/utils/printer' + +describe('Printer', () => { + describe('printGeneratedFiles', () => { + let consoleOutputSpy + + const files: string[] = [ + 'file1.html', + 'file2.html', + 'file3.html' + ] + + beforeAll(() => { + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + new Printer().printGeneratedFiles(files) + }) + + it('should print all files', () => { + expect(consoleOutputSpy).toBeCalled + }) + }) + + describe('printErrorMessage', () => { + let consoleOutputSpy + + const message = 'Test error message' + + beforeAll(() => { + consoleOutputSpy = jest.spyOn(process.stdout, 'write') + new Printer().printErrorMessage(message) + }) + + it('should print the error message', () => { + expect(consoleOutputSpy).toBeCalled + }) + }) +})