diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 569e6324d..5051e0957 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -114,7 +114,7 @@ jobs: run: npm run build - name: Execute test-generator working-directory: amplify-codegen-ui-staging/packages/test-generator - run: node . + run: node ./dist/lib/generators/GenerateTestApp.js - name: Create test app with dependencies working-directory: . run: | diff --git a/packages/test-generator/index.ts b/packages/test-generator/index.ts index 0a92f29d6..7faaace27 100644 --- a/packages/test-generator/index.ts +++ b/packages/test-generator/index.ts @@ -1,83 +1,6 @@ -import { StudioComponent } from '@amzn/amplify-ui-codegen-schema'; -import { StudioTemplateRendererManager, StudioTemplateRendererFactory } from '@amzn/studio-ui-codegen'; -import { - AmplifyRenderer, - ReactOutputConfig, - ReactRenderConfig, - ModuleKind, - ScriptTarget, - ScriptKind, -} from '@amzn/studio-ui-codegen-react'; -import path from 'path'; -import log from 'loglevel'; -import { ComponentSchemas } from './lib'; +import { TestGenerator } from './lib/generators/TestGenerator'; -Error.stackTraceLimit = Infinity; - -log.setLevel('info'); - -const renderConfig: ReactRenderConfig = { - module: ModuleKind.CommonJS, - target: ScriptTarget.ES2015, - script: ScriptKind.TSX, -}; - -const componentRendererFactory = new StudioTemplateRendererFactory( - (component: StudioComponent) => new AmplifyRenderer(component, renderConfig), -); - -// const themeRendererFactory = new StudioTemplateRendererFactory( -// (theme: StudioTheme) => new ReactThemeStudioTemplateRenderer(theme, renderConfig), -// ); - -const outputPathDir = path.resolve(path.join(__dirname, '..', 'test-app-templates', 'src', 'ui-components')); -const outputConfig: ReactOutputConfig = { - outputPathDir, -}; - -const rendererManager = new StudioTemplateRendererManager(componentRendererFactory, outputConfig); -// const themeRendererManager = new StudioTemplateRendererManager(themeRendererFactory, outputConfig); - -const decorateTypescriptWithMarkdown = (typescriptSource: string): string => { - return `\`\`\`typescript jsx\n${typescriptSource}\n\`\`\``; -}; - -Object.entries(ComponentSchemas).forEach(([name, schema]) => { - log.info(`# ${name}`); - try { - rendererManager.renderSchemaToTemplate(schema as any); - const buildRenderer = componentRendererFactory.buildRenderer(schema as any); - - const compOnly = buildRenderer.renderComponentOnly(); - log.info('## Component Only Output'); - log.info('### componentImports'); - log.info(decorateTypescriptWithMarkdown(compOnly.importsText)); - log.info('### componentText'); - log.info(decorateTypescriptWithMarkdown(compOnly.compText)); - - const compOnlyAppSample = buildRenderer.renderSampleCodeSnippet(); - log.info('## Code Snippet Output'); - log.info('### componentImports'); - log.info(decorateTypescriptWithMarkdown(compOnlyAppSample.importsText)); - log.info('### componentText'); - log.info(decorateTypescriptWithMarkdown(compOnlyAppSample.compText)); - } catch (err) { - log.error(`${name} failed with error:`); - log.error(err); - } -}); -// -// Object.entries(ThemeSchemas).forEach(([name, schema]) => { -// log.info(`# ${name}`); -// try { -// themeRendererManager.renderSchemaToTemplate(schema as any); -// const buildRenderer = themeRendererFactory.buildRenderer(schema as any); -// -// const component = buildRenderer.renderComponent(); -// log.info('## Theme Output'); -// log.info(decorateTypescriptWithMarkdown(component.componentText)); -// } catch (err) { -// log.error(`${name} failed with error:`); -// log.error(err); -// } -// }); +new TestGenerator({ + writeToLogger: true, + writeToDisk: false, +}).generate(); diff --git a/packages/test-generator/lib/components/boxWithButton.json b/packages/test-generator/lib/components/boxWithButton.json index 8d4246481..dd3b2662d 100644 --- a/packages/test-generator/lib/components/boxWithButton.json +++ b/packages/test-generator/lib/components/boxWithButton.json @@ -8,13 +8,19 @@ "id": "0987-6543-3211", "componentType": "Button", "properties": { - "color": { - "value": "#ff0000" - }, - "width": { - "value": "20px" + }, + "children": [ + { + "id": "1234-5678-9010", + "componentType": "Text", + "name": "CustomText", + "properties": { + "value": { + "value": "Text in Button" + } + } } - } + ] } ] } diff --git a/packages/test-generator/lib/components/buttonWithConcatenatedText.json b/packages/test-generator/lib/components/buttonWithConcatenatedText.json new file mode 100644 index 000000000..bb65948b2 --- /dev/null +++ b/packages/test-generator/lib/components/buttonWithConcatenatedText.json @@ -0,0 +1,47 @@ +{ + "id": "1234-5678-9010", + "componentType": "Button", + "name": "ButtonWithConcatenatedText", + "bindingProperties": { + "width": { + "type": "Number" + }, + "buttonUser": { + "type": "Data", + "bindingProperties": { + "model": "User" + } + }, + "buttonColor": { + "type": "String" + } + }, + "properties": { + "label": { + "concat": [ + { + "bindingProperties": { + "property": "buttonUser", + "field": "firstname" + }, + "defaultValue": "Harry" + }, + { + "value": " " + }, + { + "bindingProperties": { + "property": "buttonUser", + "field": "lastname" + }, + "defaultValue": "Callahan" + } + ] + }, + "labelWidth": { + "bindingProperties": { + "property": "width" + } + } + } +} diff --git a/packages/test-generator/lib/components/buttonWithConditionalState.json b/packages/test-generator/lib/components/buttonWithConditionalState.json new file mode 100644 index 000000000..3e4938aec --- /dev/null +++ b/packages/test-generator/lib/components/buttonWithConditionalState.json @@ -0,0 +1,105 @@ +{ + "id": "1234-5678-9010", + "componentType": "Button", + "name": "ButtonWithConditionalState", + "bindingProperties": { + "width": { + "type": "Number" + }, + "buttonUser": { + "type": "Data", + "bindingProperties": { + "model": "User" + } + }, + "buttonColor": { + "type": "String" + } + }, + "properties": { + "label": { + "concat": [ + { + "bindingProperties": { + "property": "buttonUser", + "field": "firstname" + }, + "defaultValue": "Harry" + }, + { + "value": " " + }, + { + "bindingProperties": { + "property": "buttonUser", + "field": "lastname" + }, + "defaultValue": "Callahan" + } + ] + }, + "labelWidth": { + "bindingProperties": { + "property": "width" + } + }, + "disabled": { + "condition": { + "property": "buttonUser", + "field": "isLoggedIn", + "operator": "eq", + "operand": true, + "then": { + "value": true + }, + "else": { + "value": false + } + } + }, + "prompt": { + "condition": { + "property": "buttonUser", + "field": "age", + "operator": "gt", + "operand": 18, + "then": { + "concat": [ + { + "bindingProperties": { + "property": "buttonUser", + "field": "firstname" + } + }, + { + "value": ", cast your vote." + } + ] + }, + "else": { + "value": "Sorry you cannot vote" + } + } + }, + "backgroundColor": { + "condition": { + "property": "buttonUser", + "field": "isLoggedIn", + "operator": "eq", + "operand": true, + "then": { + "bindingProperties": { + "property": "buttonUser", + "field": "loggedInColor" + } + }, + "else": { + "bindingProperties": { + "property": "buttonUser", + "field": "loggedOutColor" + } + } + } + } + } +} diff --git a/packages/test-generator/lib/components/customText.json b/packages/test-generator/lib/components/customText.json index ed654d941..0850ef56c 100644 --- a/packages/test-generator/lib/components/customText.json +++ b/packages/test-generator/lib/components/customText.json @@ -10,7 +10,7 @@ "value": "20px" }, "value": { - "value": "Text Value" + "value": "Custom Text Value" } } } diff --git a/packages/test-generator/lib/components/index.ts b/packages/test-generator/lib/components/index.ts index 1efd2f093..b5ab450d2 100644 --- a/packages/test-generator/lib/components/index.ts +++ b/packages/test-generator/lib/components/index.ts @@ -4,3 +4,5 @@ export { default as CustomButton } from './customButton.json'; export { default as BoxWithButtonExposedAs } from './boxWithButtonExposedAs.json'; export { default as CustomText } from './customText.json'; export { default as TextWithDataBinding } from './textWithDataBinding.json'; +export { default as ButtonWithConcatenatedText } from './buttonWithConcatenatedText.json'; +export { default as ButtonWithConditionalState } from './buttonWithConditionalState.json'; diff --git a/packages/test-generator/lib/generators/GenerateTestApp.ts b/packages/test-generator/lib/generators/GenerateTestApp.ts new file mode 100644 index 000000000..59a9ac219 --- /dev/null +++ b/packages/test-generator/lib/generators/GenerateTestApp.ts @@ -0,0 +1,11 @@ +import { TestGenerator } from './TestGenerator'; + +new TestGenerator({ + writeToLogger: false, + writeToDisk: true, + disabledSchemas: [ + 'ButtonWithConditionalState', // TODO: Fix Conditional + 'ButtonWithConcatenatedText', // TODO: Fix Concatenation + 'ExampleTheme', // TODO: Fix Themes + ], +}).generate(); diff --git a/packages/test-generator/lib/generators/TestGenerator.ts b/packages/test-generator/lib/generators/TestGenerator.ts new file mode 100644 index 000000000..69fbf3659 --- /dev/null +++ b/packages/test-generator/lib/generators/TestGenerator.ts @@ -0,0 +1,128 @@ +import { StudioComponent, StudioTheme } from '@amzn/amplify-ui-codegen-schema'; +import { StudioTemplateRendererManager, StudioTemplateRendererFactory } from '@amzn/studio-ui-codegen'; +import { + AmplifyRenderer, + ReactOutputConfig, + ModuleKind, + ScriptTarget, + ScriptKind, + ReactThemeStudioTemplateRenderer, + ReactRenderConfig, +} from '@amzn/studio-ui-codegen-react'; +import path from 'path'; +import log from 'loglevel'; +import { ComponentSchemas, ThemeSchemas } from '../index'; + +const DEFAULT_RENDER_CONFIG = { + module: ModuleKind.CommonJS, + target: ScriptTarget.ES2015, + script: ScriptKind.TSX, +}; + +Error.stackTraceLimit = Infinity; + +log.setLevel('info'); + +export type TestGeneratorParams = { + writeToLogger: boolean; + writeToDisk: boolean; + renderConfigOverride?: ReactRenderConfig; + disabledSchemas?: string[]; +}; + +export class TestGenerator { + private readonly params: TestGeneratorParams; + + private readonly componentRendererFactory: any; + + private readonly themeRendererFactory: any; + + private readonly rendererManager: any; + + private readonly themeRendererManager: any; + + constructor(params: TestGeneratorParams) { + this.params = params; + const mergedRenderConfig = { ...DEFAULT_RENDER_CONFIG, ...params.renderConfigOverride }; + this.componentRendererFactory = new StudioTemplateRendererFactory( + (component: StudioComponent) => new AmplifyRenderer(component, mergedRenderConfig), + ); + this.themeRendererFactory = new StudioTemplateRendererFactory( + (theme: StudioTheme) => new ReactThemeStudioTemplateRenderer(theme, mergedRenderConfig), + ); + const outputPathDir = path.resolve( + path.join(__dirname, '..', '..', '..', 'test-app-templates', 'src', 'ui-components'), + ); + const outputConfig: ReactOutputConfig = { outputPathDir }; + this.rendererManager = new StudioTemplateRendererManager(this.componentRendererFactory, outputConfig); + this.themeRendererManager = new StudioTemplateRendererManager(this.themeRendererFactory, outputConfig); + } + + private decorateTypescriptWithMarkdown = (typescriptSource: string): string => { + return `\`\`\`typescript jsx\n${typescriptSource}\n\`\`\``; + }; + + generate = () => { + const renderErrors: { [key: string]: any } = {}; + + Object.entries(ComponentSchemas).forEach(([name, schema]) => { + if (this.params.disabledSchemas && this.params.disabledSchemas.includes(name)) { + return; + } + try { + if (this.params.writeToDisk) { + this.rendererManager.renderSchemaToTemplate(schema as any); + } + + if (this.params.writeToLogger) { + const buildRenderer = this.componentRendererFactory.buildRenderer(schema as any); + const compOnly = buildRenderer.renderComponentOnly(); + const compOnlyAppSample = buildRenderer.renderSampleCodeSnippet(); + log.info(`# ${name}`); + log.info('## Component Only Output'); + log.info('### componentImports'); + log.info(this.decorateTypescriptWithMarkdown(compOnly.importsText)); + log.info('### componentText'); + log.info(this.decorateTypescriptWithMarkdown(compOnly.compText)); + log.info('## Code Snippet Output'); + log.info('### componentImports'); + log.info(this.decorateTypescriptWithMarkdown(compOnlyAppSample.importsText)); + log.info('### componentText'); + log.info(this.decorateTypescriptWithMarkdown(compOnlyAppSample.compText)); + } + } catch (err) { + renderErrors[name] = err; + } + }); + + Object.entries(ThemeSchemas).forEach(([name, schema]) => { + if (this.params.disabledSchemas && this.params.disabledSchemas.includes(name)) { + return; + } + try { + if (this.params.writeToDisk) { + this.themeRendererManager.renderSchemaToTemplate(schema as any); + } + + if (this.params.writeToLogger) { + const buildRenderer = this.themeRendererFactory.buildRenderer(schema as any); + const component = buildRenderer.renderComponent(); + log.info(`# ${name}`); + log.info('## Theme Output'); + log.info(this.decorateTypescriptWithMarkdown(component.componentText)); + } + } catch (err) { + renderErrors[name] = err; + } + }); + + if (Object.keys(renderErrors).length > 0) { + log.error('Caught exceptions while rendering templates'); + Object.entries(renderErrors).forEach(([name, error]) => { + log.error(`Schema: ${name}`); + log.error(error); + }); + throw new Error('Not all tests rendered successfully'); + } + }; +} diff --git a/packages/test-generator/test-app-templates/cypress/integration/generated-components-spec.js b/packages/test-generator/test-app-templates/cypress/integration/generated-components-spec.js index a8ab3f78a..81009f1ca 100644 --- a/packages/test-generator/test-app-templates/cypress/integration/generated-components-spec.js +++ b/packages/test-generator/test-app-templates/cypress/integration/generated-components-spec.js @@ -1,5 +1,59 @@ -describe('My First Tests', () => { - it('Opens the test app', () => { +describe('Generated Components', () => { + describe('Sanity Test', () => { + it('Successfully opens the app', () => { + cy.visit('http://localhost:3000'); + }); + }); + + describe('Basic Components', () => { + it('Renders Box with Button, and text inside', () => { + cy.visit('http://localhost:3000'); + cy.get('button').contains('Text in Button'); + }); + + it('Renders Text component', () => { + cy.visit('http://localhost:3000'); + cy.contains('Custom Text Value'); + }); + }); + + describe('Conditional Data', () => { + // TODO: Write Conditional Cases + }); + + describe('Concatenated Data', () => { + // TODO: Get Concatenation Cases Working + // it('Renders Button text as a concatenated, bound element', () => { + // cy.visit('http://localhost:3000'); + // cy.contains('Harry Callahan') + // }); + // + // it('Renders Button text as a concatenated, bound element, with overrides', () => { + // cy.visit('http://localhost:3000'); + // cy.contains('Norm Gunderson') + // }); + }); + + describe('Component Variants', () => { + // TODO: Write Variant Cases + }); + + describe('Data Binding', () => { + // TODO: Write Data Binding Cases + }); + + describe('Action Binding', () => { + // TODO: Write Action Binding Cases + }); + + describe('Collections', () => { + // TODO: Write Collection Cases + }); +}); + +describe('Generated Themes', () => { + it('Successfully decorates the app', () => { cy.visit('http://localhost:3000'); + // TODO: Write theming test }); }); diff --git a/packages/test-generator/test-app-templates/src/App.tsx b/packages/test-generator/test-app-templates/src/App.tsx index f44e4d3ad..69242270d 100644 --- a/packages/test-generator/test-app-templates/src/App.tsx +++ b/packages/test-generator/test-app-templates/src/App.tsx @@ -6,17 +6,63 @@ import BoxWithButtonExposedAs from './ui-components/BoxWithButtonExposedAs'; import CustomButton from './ui-components/CustomButton'; import CustomText from './ui-components/CustomText'; import TextWithDataBinding from './ui-components/TextWithDataBinding'; +// import ButtonWithConcatenatedText from './ui-components/ButtonWithConcatenatedText'; +// import ButtonWithConditionalState from './ui-components/ButtonWithConditionalState'; /* eslint-enable import/extensions */ function App() { return ( <> - <BoxTest></BoxTest> - <BoxWithButton></BoxWithButton> - <BoxWithButtonExposedAs></BoxWithButtonExposedAs> - <CustomButton></CustomButton> - <CustomText></CustomText> - <TextWithDataBinding></TextWithDataBinding> + <BoxTest /> + <BoxWithButton /> + <BoxWithButtonExposedAs /> + <CustomButton /> + <CustomText /> + <TextWithDataBinding /> + {/* + TODO: buttonUser Listed as optional prop, but fails when not present + <ButtonWithConcatenatedText /> + <ButtonWithConcatenatedText + buttonUser={{ + firstname: 'Norm', + lastname: 'Gunderson', + isLoggedIn: true, + loggedInColor: 'blue', + loggedOutColor: 'red', + age: -1, + }} + /> + <ButtonWithConditionalState + buttonUser={{ + firstname: 'Disabled', + lastname: 'Conditional Button', + isLoggedIn: false, + loggedInColor: 'blue', + loggedOutColor: 'red', + age: -1, + }} + /> + <ButtonWithConditionalState + buttonUser={{ + firstname: 'May Vote', + lastname: 'Conditional Button', + age: 19, + isLoggedIn: true, + loggedInColor: 'blue', + loggedOutColor: 'red', + }} + /> + <ButtonWithConditionalState + buttonUser={{ + firstname: 'May Not Vote', + lastname: 'Conditional Button', + age: 16, + isLoggedIn: true, + loggedInColor: 'blue', + loggedOutColor: 'red', + }} + /> + */} </> ); } diff --git a/packages/test-generator/test-app-templates/src/models/User.ts b/packages/test-generator/test-app-templates/src/models/User.ts new file mode 100644 index 000000000..21ddabb08 --- /dev/null +++ b/packages/test-generator/test-app-templates/src/models/User.ts @@ -0,0 +1,8 @@ +export type User = { + firstname: string; + lastname: string; + isLoggedIn: boolean; + age: number; + loggedInColor: string; + loggedOutColor: string; +}; diff --git a/packages/test-generator/test-app-templates/src/models/index.ts b/packages/test-generator/test-app-templates/src/models/index.ts new file mode 100644 index 000000000..f6b9f36c6 --- /dev/null +++ b/packages/test-generator/test-app-templates/src/models/index.ts @@ -0,0 +1 @@ +export * from './User';