From 8416b043eaef9f3b53e488168f6451de1fe0b6a2 Mon Sep 17 00:00:00 2001 From: Sofiia Date: Fri, 15 Jun 2018 04:06:20 +0200 Subject: [PATCH] housekeeping: factor out components for easier BaseLayout reuse (#4604) * improve: wrap schemes to encapsulate rendering logic * improve: wrap filter to encapsulate rendering logic * improve: wrap info section to encapsulate rendering logic * improve: wrap servers plugin to encapsulate rendering logic * improve: added tests for schemes-wrapper rendering logic * improve: added tests for info-wrapper rendering logic, also do not render info if info is undefined * improve: added tests for filter rendering logic * improve: added tests for servers-wrapper rendering logic * `InfoWrapper` -> `InfoContainer` * add `containers` alias to Babel configuration * `SchemesWrapper` -> `SchemesContainer` * drop `container` from container file names * `ServersWrapper` -> `ServersContainer` * `Filter` -> `FilterContainer` * follow `core/containers` pattern in BasePreset --- .babelrc | 4 + src/core/components/layouts/base.jsx | 90 ++----------- src/core/containers/filter.jsx | 44 +++++++ src/core/containers/info.jsx | 32 +++++ src/core/containers/schemes.jsx | 43 +++++++ src/core/plugins/oas3/components/index.js | 2 + .../oas3/components/servers-container.jsx | 42 +++++++ src/core/presets/base.js | 6 + test/components/filter.js | 65 ++++++++++ test/components/info-wrapper.js | 67 ++++++++++ test/components/schemes-wrapper.js | 119 ++++++++++++++++++ test/core/plugins/oas3/servers-wrapper.js | 77 ++++++++++++ 12 files changed, 511 insertions(+), 80 deletions(-) create mode 100644 src/core/containers/filter.jsx create mode 100644 src/core/containers/info.jsx create mode 100644 src/core/containers/schemes.jsx create mode 100644 src/core/plugins/oas3/components/servers-container.jsx create mode 100644 test/components/filter.js create mode 100644 test/components/info-wrapper.js create mode 100644 test/components/schemes-wrapper.js create mode 100644 test/core/plugins/oas3/servers-wrapper.js diff --git a/.babelrc b/.babelrc index 2b14751a..57276240 100644 --- a/.babelrc +++ b/.babelrc @@ -18,6 +18,10 @@ "expose": "components", "src": "src/core/components" }, + { + "expose": "containers", + "src": "src/core/containers" + }, { "expose": "core", "src": "src/core" diff --git a/src/core/components/layouts/base.jsx b/src/core/components/layouts/base.jsx index d0d56b37..f1d09b89 100644 --- a/src/core/components/layouts/base.jsx +++ b/src/core/components/layouts/base.jsx @@ -6,67 +6,35 @@ export default class BaseLayout extends React.Component { static propTypes = { errSelectors: PropTypes.object.isRequired, errActions: PropTypes.object.isRequired, - specActions: PropTypes.object.isRequired, specSelectors: PropTypes.object.isRequired, oas3Selectors: PropTypes.object.isRequired, oas3Actions: PropTypes.object.isRequired, - layoutSelectors: PropTypes.object.isRequired, - layoutActions: PropTypes.object.isRequired, getComponent: PropTypes.func.isRequired } - onFilterChange =(e) => { - let {target: {value}} = e - this.props.layoutActions.updateFilter(value) - } - render() { - let { - specSelectors, - specActions, - getComponent, - layoutSelectors, - oas3Selectors, - oas3Actions - } = this.props - - let info = specSelectors.info() - let url = specSelectors.url() - let basePath = specSelectors.basePath() - let host = specSelectors.host() - let securityDefinitions = specSelectors.securityDefinitions() - let externalDocs = specSelectors.externalDocs() - let schemes = specSelectors.schemes() - let servers = specSelectors.servers() + let {specSelectors, getComponent} = this.props let SvgAssets = getComponent("SvgAssets") + let InfoContainer = getComponent("InfoContainer", true) let VersionPragmaFilter = getComponent("VersionPragmaFilter") - let Info = getComponent("info") let Operations = getComponent("operations", true) let Models = getComponent("Models", true) - let AuthorizeBtn = getComponent("authorizeBtn", true) let Row = getComponent("Row") let Col = getComponent("Col") - let Servers = getComponent("Servers") + let ServersContainer = getComponent("ServersContainer", true) let Errors = getComponent("errors", true) + const SchemesContainer = getComponent("SchemesContainer", true) + const FilterContainer = getComponent("FilterContainer", true) let isSwagger2 = specSelectors.isSwagger2() let isOAS3 = specSelectors.isOAS3() - let isLoading = specSelectors.loadingStatus() === "loading" - let isFailed = specSelectors.loadingStatus() === "failed" - let filter = layoutSelectors.currentFilter() - - let inputStyle = {} - if(isFailed) inputStyle.color = "red" - if(isLoading) inputStyle.color = "#aaa" - - const Schemes = getComponent("schemes") - const isSpecEmpty = !specSelectors.specStr() if(isSpecEmpty) { let loadingMessage + let isLoading = specSelectors.loadingStatus() === "loading" if(isLoading) { loadingMessage =
} else { @@ -88,53 +56,15 @@ export default class BaseLayout extends React.Component { - { info.count() ? ( - - ) : null } + - { schemes && schemes.size || securityDefinitions ? ( -
- - { schemes && schemes.size ? ( - - ) : null } - { securityDefinitions ? ( - - ) : null } - -
- ) : null } + - { servers && servers.size ? ( -
- - Server - - -
+ - ) : null} - - { - filter === null || filter === false ? null : -
- - - -
- } + diff --git a/src/core/containers/filter.jsx b/src/core/containers/filter.jsx new file mode 100644 index 00000000..8cfdcff9 --- /dev/null +++ b/src/core/containers/filter.jsx @@ -0,0 +1,44 @@ +import React from "react" +import PropTypes from "prop-types" + +export default class FilterContainer extends React.Component { + + static propTypes = { + specSelectors: PropTypes.object.isRequired, + layoutSelectors: PropTypes.object.isRequired, + layoutActions: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + } + + onFilterChange = (e) => { + const {target: {value}} = e + this.props.layoutActions.updateFilter(value) + } + + render () { + const {specSelectors, layoutSelectors, getComponent} = this.props + const Col = getComponent("Col") + + const isLoading = specSelectors.loadingStatus() === "loading" + const isFailed = specSelectors.loadingStatus() === "failed" + const filter = layoutSelectors.currentFilter() + + const inputStyle = {} + if (isFailed) inputStyle.color = "red" + if (isLoading) inputStyle.color = "#aaa" + + return ( +
+ {filter === null || filter === false ? null : +
+ + + +
+ } +
+ ) + } +} diff --git a/src/core/containers/info.jsx b/src/core/containers/info.jsx new file mode 100644 index 00000000..4ae558c7 --- /dev/null +++ b/src/core/containers/info.jsx @@ -0,0 +1,32 @@ +import React from "react" +import PropTypes from "prop-types" + +export default class InfoContainer extends React.Component { + + static propTypes = { + specActions: PropTypes.object.isRequired, + specSelectors: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + } + + render () { + const {specSelectors, getComponent} = this.props + + const info = specSelectors.info() + const url = specSelectors.url() + const basePath = specSelectors.basePath() + const host = specSelectors.host() + const externalDocs = specSelectors.externalDocs() + + const Info = getComponent("info") + + return ( +
+ {info && info.count() ? ( + + ) : null} +
+ ) + } +} diff --git a/src/core/containers/schemes.jsx b/src/core/containers/schemes.jsx new file mode 100644 index 00000000..8d64c283 --- /dev/null +++ b/src/core/containers/schemes.jsx @@ -0,0 +1,43 @@ +import React from "react" +import PropTypes from "prop-types" + +export default class SchemesContainer extends React.Component { + + static propTypes = { + specActions: PropTypes.object.isRequired, + specSelectors: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + } + + render () { + const {specActions, specSelectors, getComponent} = this.props + const currentScheme = specSelectors.operationScheme() + const schemes = specSelectors.schemes() + const securityDefinitions = specSelectors.securityDefinitions() + + const Col = getComponent("Col") + const AuthorizeBtn = getComponent("authorizeBtn", true) + const Schemes = getComponent("schemes") + + return ( +
+ {schemes && schemes.size || securityDefinitions ? ( +
+ + {schemes && schemes.size ? ( + + ) : null} + {securityDefinitions ? ( + + ) : null} + +
+ ) : null} +
+ ) + } +} diff --git a/src/core/plugins/oas3/components/index.js b/src/core/plugins/oas3/components/index.js index b37817a8..d5f58c8e 100644 --- a/src/core/plugins/oas3/components/index.js +++ b/src/core/plugins/oas3/components/index.js @@ -2,6 +2,7 @@ import Callbacks from "./callbacks" import RequestBody from "./request-body" import OperationLink from "./operation-link.jsx" import Servers from "./servers" +import ServersContainer from "./servers-container" import RequestBodyEditor from "./request-body-editor" import HttpAuth from "./http-auth" import OperationServers from "./operation-servers" @@ -11,6 +12,7 @@ export default { HttpAuth, RequestBody, Servers, + ServersContainer, RequestBodyEditor, OperationServers, operationLink: OperationLink diff --git a/src/core/plugins/oas3/components/servers-container.jsx b/src/core/plugins/oas3/components/servers-container.jsx new file mode 100644 index 00000000..8476496a --- /dev/null +++ b/src/core/plugins/oas3/components/servers-container.jsx @@ -0,0 +1,42 @@ +import React from "react" +import PropTypes from "prop-types" + +export default class ServersContainer extends React.Component { + + static propTypes = { + specSelectors: PropTypes.object.isRequired, + oas3Selectors: PropTypes.object.isRequired, + oas3Actions: PropTypes.object.isRequired, + getComponent: PropTypes.func.isRequired, + } + + render () { + const {specSelectors, oas3Selectors, oas3Actions, getComponent} = this.props + + const servers = specSelectors.servers() + + const Col = getComponent("Col") + const Servers = getComponent("Servers") + + return ( +
+ {servers && servers.size ? ( +
+ + Server + + +
+ + ) : null} +
+ ) + } +} diff --git a/src/core/presets/base.js b/src/core/presets/base.js index 8a7812d7..77cabf59 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -51,10 +51,13 @@ import Info, { InfoUrl, InfoBasePath } from "core/components/info" +import InfoContainer from "core/containers/info" import Footer from "core/components/footer" +import FilterContainer from "core/containers/filter" import ParamBody from "core/components/param-body" import Curl from "core/components/curl" import Schemes from "core/components/schemes" +import SchemesContainer from "core/containers/schemes" import ModelCollapse from "core/components/model-collapse" import ModelExample from "core/components/model-example" import ModelWrapper from "core/components/model-wrapper" @@ -95,6 +98,7 @@ export default function() { clear: Clear, liveResponse: LiveResponse, info: Info, + InfoContainer, onlineValidatorBadge: OnlineValidatorBadge, operations: Operations, operation: Operation, @@ -110,9 +114,11 @@ export default function() { contentType: ContentType, overview: Overview, footer: Footer, + FilterContainer, ParamBody: ParamBody, curl: Curl, schemes: Schemes, + SchemesContainer, modelExample: ModelExample, ModelWrapper, ModelCollapse, diff --git a/test/components/filter.js b/test/components/filter.js new file mode 100644 index 00000000..6a7ef1ac --- /dev/null +++ b/test/components/filter.js @@ -0,0 +1,65 @@ + +/* eslint-env mocha */ +import React from "react" +import expect from "expect" +import { mount } from "enzyme" +import FilterContainer from "containers/filter" +import { Col } from "components/layout-utils" + +describe("", function(){ + + const mockedProps = { + specSelectors: { + loadingStatus() {} + }, + layoutSelectors: { + currentFilter() {} + }, + getComponent: () => {return Col} + } + + it("renders FilterContainer if filter is provided", function(){ + + // Given + let props = {...mockedProps} + props.layoutSelectors = {...mockedProps.specSelectors} + props.layoutSelectors.currentFilter = function() {return true} + + // When + let wrapper = mount() + + // Then + const renderedColInsideFilter = wrapper.find(Col) + expect(renderedColInsideFilter.length).toEqual(1) + }) + + it("does not render FilterContainer if filter is null", function(){ + + // Given + let props = {...mockedProps} + props.layoutSelectors = {...mockedProps.specSelectors} + props.layoutSelectors.currentFilter = function() {return null} + + // When + let wrapper = mount() + + // Then + const renderedColInsideFilter = wrapper.find(Col) + expect(renderedColInsideFilter.length).toEqual(0) + }) + + it("does not render FilterContainer if filter is false", function(){ + + // Given + let props = {...mockedProps} + props.layoutSelectors = {...mockedProps.specSelectors} + props.layoutSelectors.currentFilter = function() {return false} + + // When + let wrapper = mount() + + // Then + const renderedColInsideFilter = wrapper.find(Col) + expect(renderedColInsideFilter.length).toEqual(0) + }) +}) diff --git a/test/components/info-wrapper.js b/test/components/info-wrapper.js new file mode 100644 index 00000000..fcb98d6f --- /dev/null +++ b/test/components/info-wrapper.js @@ -0,0 +1,67 @@ + +/* eslint-env mocha */ +import React from "react" +import expect from "expect" +import { mount } from "enzyme" +import { fromJS } from "immutable" +import InfoContainer from "containers/info" + +describe("", function () { + + const components = { + info: () => + } + const mockedProps = { + specSelectors: { + info () {}, + url () {}, + basePath () {}, + host () {}, + externalDocs () {} + }, + getComponent: c => components[c] + } + + it("renders Info inside InfoContainer if info is provided", function () { + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.info = function () {return fromJS(["info1", "info2"])} + + // When + let wrapper = mount() + + // Then + const renderedInfo = wrapper.find("span.mocked-info") + expect(renderedInfo.length).toEqual(1) + }) + + it("does not render Info inside InfoContainer if no info is provided", function () { + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.info = function () {return fromJS([])} + + // When + let wrapper = mount() + + // Then + const renderedInfo = wrapper.find("span.mocked-info") + expect(renderedInfo.length).toEqual(0) + }) + + it("does not render Info inside InfoContainer if info is undefined", function () { + + // Given + let props = {...mockedProps} + + // When + let wrapper = mount() + + // Then + const renderedInfo = wrapper.find("span.mocked-info") + expect(renderedInfo.length).toEqual(0) + }) +}) diff --git a/test/components/schemes-wrapper.js b/test/components/schemes-wrapper.js new file mode 100644 index 00000000..bc2692a5 --- /dev/null +++ b/test/components/schemes-wrapper.js @@ -0,0 +1,119 @@ + +/* eslint-env mocha */ +import React from "react" +import expect from "expect" +import { mount, render } from "enzyme" +import { fromJS } from "immutable" +import SchemesContainer from "containers/schemes" +import Schemes from "components/schemes" +import { Col } from "components/layout-utils" + +describe("", function(){ + + const components = { + schemes: Schemes, + Col, + authorizeBtn: () => + } + const mockedProps = { + specSelectors: { + securityDefinitions() {}, + operationScheme() {}, + schemes() {} + }, + specActions: { + setScheme() {} + }, + getComponent: c => components[c] + } + const twoSecurityDefinitions = { + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + + it("renders Schemes inside SchemesContainer if schemes are provided", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.operationScheme = function() {return "http"} + props.specSelectors.schemes = function() {return fromJS(["http", "https"])} + + // When + let wrapper = mount() + + // Then + const renderedSchemes = wrapper.find(Schemes) + expect(renderedSchemes.length).toEqual(1) + }) + + it("does not render Schemes inside SchemeWrapper if empty array of schemes is provided", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.schemes = function() {return fromJS([])} + + // When + let wrapper = mount() + + // Then + const renderedSchemes = wrapper.find(Schemes) + expect(renderedSchemes.length).toEqual(0) + }) + + it("does not render Schemes inside SchemeWrapper if provided schemes are undefined", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.schemes = function() {return undefined} + + // When + let wrapper = mount() + + // Then + const renderedSchemes = wrapper.find(Schemes) + expect(renderedSchemes.length).toEqual(0) + }) + + it("renders AuthorizeBtn inside SchemesContainer if security definition is provided", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.securityDefinitions = function () {return fromJS(twoSecurityDefinitions)} + + // When + let wrapper = render() + + // Then + const renderedAuthorizeBtn = wrapper.find("span.mocked-button") + expect(renderedAuthorizeBtn.length).toEqual(1) + }) + + it("does not render AuthorizeBtn if security definition is not provided", function(){ + + // Given + let props = {...mockedProps} + + // When + let wrapper = render() + + // Then + const renderedAuthorizeBtn = wrapper.find("span.mocked-button") + expect(renderedAuthorizeBtn.length).toEqual(0) + }) +}) diff --git a/test/core/plugins/oas3/servers-wrapper.js b/test/core/plugins/oas3/servers-wrapper.js new file mode 100644 index 00000000..aeece8c1 --- /dev/null +++ b/test/core/plugins/oas3/servers-wrapper.js @@ -0,0 +1,77 @@ + +/* eslint-env mocha */ +import React from "react" +import expect from "expect" +import { mount } from "enzyme" +import { fromJS } from "immutable" +import ServersContainer from "core/plugins/oas3/components/servers-container" +import Servers from "core/plugins/oas3/components/servers" +import { Col } from "components/layout-utils" + +describe("", function(){ + + const components = { + Servers, + Col + } + const mockedProps = { + specSelectors: { + servers() {} + }, + oas3Selectors: { + selectedServer() {}, + serverVariableValue() {}, + serverEffectiveValue() {} + }, + oas3Actions: { + setSelectedServer() {}, + setServerVariableValue() {} + }, + getComponent: c => components[c] + } + + it("renders Servers inside ServersContainer if servers are provided", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.servers = function() {return fromJS([{url: "http://server1.com"}])} + props.oas3Selectors = {...mockedProps.oas3Selectors} + props.oas3Selectors.selectedServer = function() {return "http://server1.com"} + + // When + let wrapper = mount() + + // Then + const renderedServers = wrapper.find(Servers) + expect(renderedServers.length).toEqual(1) + }) + + it("does not render Servers inside ServersContainer if servers are empty", function(){ + + // Given + let props = {...mockedProps} + props.specSelectors = {...mockedProps.specSelectors} + props.specSelectors.servers = function() {return fromJS([])} + + // When + let wrapper = mount() + + // Then + const renderedServers = wrapper.find(Servers) + expect(renderedServers.length).toEqual(0) + }) + + it("does not render Servers inside ServersContainer if servers are undefined", function(){ + + // Given + let props = {...mockedProps} + + // When + let wrapper = mount() + + // Then + const renderedServers = wrapper.find(Servers) + expect(renderedServers.length).toEqual(0) + }) +})