Compare commits

...
17 Commits
Author SHA1 Message Date
kyle 1013d16d9e v3.10.0 (#4210)
* v3.10.0

* Fix Execute styling issue

* Rebuild dist
2018-02-09 20:47:44 -08:00
gilbode f0c5f1fe33 fix: use mutated request for request URL in live-response (#4117) 2018-02-09 19:49:18 -08:00
Scott O'Hara f589cb99af feature: supportedSubmitMethods config parameter (#4186) 2018-02-09 19:39:43 -08:00
maryscar 3d7a00c0d0 improve: visibility & accessibility (#4202)
* Increase font weight for property-format

* Increase font weight for model property examples.
2018-02-09 19:34:38 -08:00
kyle 1f44231df9 improve: remove CPU-taxing Execute button animation (#4204) 2018-02-07 21:07:10 -08:00
Helder Sepulveda f2a8ed07e0 improve(sanitizer): add more allowed attributes (#4194)
* Add a couple of items to the sanitizeOptions

*  Strings must use doublequote  quotes
2018-02-07 19:51:15 -08:00
kyle 7fd229fe72 improve(docs): use computed key to demonstrate statePlugin namespacing (#4193) 2018-02-05 15:22:12 -08:00
Fabian Picone d9c60d7d25 fix(docs): typo (#4183) 2018-02-02 20:56:02 -08:00
kyle 9653258bc1 v3.9.3 (#4170)
* default to empty `ImmutableMap` when grabbing op metadata

* pass `errors` into JsonSchema components

* Account for Immutable data structure in JavaScriptonSchema...

...and create empty Lists instead of Maps by default.

* Pass ImmutableList through to JsonSchema child components

* v3.9.3
2018-01-26 19:38:41 -08:00
kyle 1e1b7f6063 fix(try-it-out): pass Parameter validation messages around in props for OAS3 (#4162)
* default to empty `ImmutableMap` when grabbing op metadata

* pass `errors` into JsonSchema components

* Account for Immutable data structure in JavaScriptonSchema...

...and create empty Lists instead of Maps by default.

* Pass ImmutableList through to JsonSchema child components
2018-01-25 20:14:35 -08:00
maryscar 2f236982f8 improve(a11y): visibility accessibility via aria-label (#4127)
* Added aria-label for visual readers.

* Increased font weight for Models section span to meet contrast ratio visibility requirements.
2018-01-25 12:41:26 -08:00
Helder Sepulveda 4599033b5a fix(rendering): operation expand/collapse button title (#4130) 2018-01-24 16:48:04 -08:00
Rafael Lüder 6110a7da12 fix(docs): typos! (#4148) 2018-01-24 16:44:46 -08:00
kyle 10ee5c46c0 improve(pr-template) (#4156) 2018-01-24 16:40:39 -08:00
Helder Sepulveda 2ab6fcc49b fix(render-tags): always preserve small element (#4129) 2018-01-23 16:58:20 -08:00
Helen Kosova 179699277d fix(parameter-rendering): improve punctuation and spacing (#4145) 2018-01-23 16:33:27 -08:00
Mike Fidel 3f647875fd bug(docs) correct typo in installation.md (#4144) 2018-01-23 16:26:36 -08:00
27 changed files with 125 additions and 98 deletions
+26 -13
View File
@@ -24,19 +24,32 @@
### Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] No code changes (changes to documentation, CI, metadata, etc)
- [ ] Dependency changes (any modification to dependencies in `package.json`)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
### Checklist:
## Checklist
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [ ] I have added tests to cover my changes.
### My PR contains...
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] No code changes (`src/` is unmodified: changes to documentation, CI, metadata, etc.)
- [ ] Dependency changes (any modification to dependencies in `package.json`)
- [ ] Bug fixes (non-breaking change which fixes an issue)
- [ ] Improvements (misc. changes to existing features)
- [ ] Features (non-breaking change which adds functionality)
### My changes...
- [ ] are breaking changes to a public API (config options, System API, major UI change, etc).
- [ ] are breaking changes to a private API (Redux, component props, utility functions, etc.).
- [ ] are breaking changes to a developer API (npm script behavior changes, new dev system dependencies, etc).
- [ ] are not breaking changes.
### Documentation
- [ ] My changes do not require a change to the project documentation.
- [ ] My changes require a change to the project documentation.
- [ ] If yes to above: I have updated the documentation accordingly.
### Automated tests
- [ ] My changes can not or do not need to be tested.
- [ ] My changes can and should be tested by unit and/or integration tests.
- [ ] If yes to above: I have added tests to cover my changes.
- [ ] If yes to above: I have taken care to cover edge cases in my tests.
- [ ] All new and existing tests passed.
+1 -1
View File
@@ -22,7 +22,7 @@ The OpenAPI Specification has undergone 5 revisions since initial creation in 20
Swagger UI Version | Release Date | OpenAPI Spec compatibility | Notes
------------------ | ------------ | -------------------------- | -----
3.9.2 | 2018-01-19 | 2.0, 3.0 | [tag v3.9.2](https://github.com/swagger-api/swagger-ui/tree/v3.9.2)
3.10.0 | 2018-02-09 | 2.0, 3.0 | [tag v3.10.0](https://github.com/swagger-api/swagger-ui/tree/v3.10.0)
3.0.21 | 2017-07-26 | 2.0 | [tag v3.0.21](https://github.com/swagger-api/swagger-ui/tree/v3.0.21)
2.2.10 | 2017-01-04 | 1.1, 1.2, 2.0 | [tag v2.2.10](https://github.com/swagger-api/swagger-ui/tree/v2.2.10)
2.1.5 | 2016-07-20 | 1.1, 1.2, 2.0 | [tag v2.1.5](https://github.com/swagger-api/swagger-ui/tree/v2.1.5)
+10 -10
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -20,16 +20,16 @@ class OperationsLayout extends React.Component {
const Operations = getComponent("Operations", true)
return {
return (
<div>
<Operations />
</div>
}
)
}
}
// Create the plugin that provides our layout component
const OperationsLayoutPlugin = function() {
const OperationsLayoutPlugin = () => {
return {
components: {
OperationsLayout: OperationsLayout
@@ -62,19 +62,19 @@ class AugmentingLayout extends React.Component {
const BaseLayout = getComponent("BaseLayout", true)
return {
return (
<div>
<div className="myCustomHeader">
<h1>I have a custom header above Swagger-UI!</h1>
</div>
<BaseLayout />
</div>
}
)
}
}
// Create the plugin that provides our layout component
const AugmentingLayoutPlugin = function() {
const AugmentingLayoutPlugin = () => {
return {
components: {
AugmentingLayout: AugmentingLayout
+2 -2
View File
@@ -4,12 +4,12 @@ A plugin is a function that returns an object - more specifically, the object ma
### Format
A plugin return value may contain any of these keys, where `myStateKey` is a name for a piece of state:
A plugin return value may contain any of these keys, where `stateKey` is a name for a piece of state:
```javascript
{
statePlugins: {
myStateKey: {
[stateKey]: {
actions,
reducers,
selectors,
+1
View File
@@ -68,6 +68,7 @@ Parameter Name | Description
`requestInterceptor` | `Function=(a => a)`. MUST be a function. Function to intercept remote definition, Try-It-Out, and OAuth2 requests. Accepts one argument requestInterceptor(request) and must return the potentially modified request.
`responseInterceptor` |`Function=(a => a)`. MUST be a function. Function to intercept remote definition, Try-It-Out, and OAuth2 responses. Accepts one argument responseInterceptor(response) and must return the potentially modified response.
`showMutatedRequest` | `Boolean=true`. If set to `true`, uses the mutated request returned from a requestInterceptor to produce the curl command in the UI, otherwise the request before the requestInterceptor was applied is used.
`supportedSubmitMethods` | `Array=["get", "put", "post", "delete", "options", "head", "patch", "trace"]`. List of HTTP methods that have the Try it out feature enabled. An empty array disables Try it out for all operations. This does not filter the operations from the display.
`validatorUrl` | `String="https://online.swagger.io/validator" OR null`. By default, Swagger-UI attempts to validate specs against swagger.io's online validator. You can use this parameter to set a different validator URL, for example for locally deployed validators ([Validator Badge](https://github.com/swagger-api/validator-badge)). Setting it to `null` will disable validation.
##### Macros
+1 -1
View File
@@ -83,7 +83,7 @@ This will serve Swagger UI at `/swagger` instead of `/`.
### unpkg
You can embed Swagger-UI's code directly in your HTML by using unkpg's interface:
You can embed Swagger-UI's code directly in your HTML by using unpkg's interface:
```html
<script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "swagger-ui",
"version": "3.9.2",
"version": "3.10.0",
"main": "dist/swagger-ui.js",
"repository": "git@github.com:swagger-api/swagger-ui.git",
"contributors": [
@@ -82,7 +82,7 @@
"scroll-to-element": "^2.0.0",
"serialize-error": "2.0.0",
"shallowequal": "0.2.2",
"swagger-client": "^3.4.6",
"swagger-client": "^3.4.11",
"url-parse": "^1.1.8",
"whatwg-fetch": "0.11.1",
"worker-loader": "^0.7.1",
@@ -20,7 +20,9 @@ export default class AuthorizeOperationBtn extends React.Component {
let { isAuthorized } = this.props
return (
<button className={isAuthorized ? "authorization__btn locked" : "authorization__btn unlocked"} onClick={ this.onClick }>
<button className={isAuthorized ? "authorization__btn locked" : "authorization__btn unlocked"}
aria-label={isAuthorized ? "authorization button locked" : "authorization button unlocked"}
onClick={this.onClick}>
<svg width="20" height="20">
<use href={ isAuthorized ? "#locked" : "#unlocked" } xlinkHref={ isAuthorized ? "#locked" : "#unlocked" } />
</svg>
+1 -1
View File
@@ -53,7 +53,7 @@ export default class LiveResponse extends React.Component {
const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(path, method) : specSelectors.requestFor(path, method)
const status = response.get("status")
const url = response.get("url")
const url = curlRequest.get("url")
const headers = response.get("headers").toJS()
const notDocumented = response.get("notDocumented")
const isError = response.get("error")
+2 -2
View File
@@ -86,7 +86,7 @@ export default class Operations extends React.Component {
isShown={showTag}
path={tag}
text={tag} />
{ !tagDescription ? null :
{ !tagDescription ? <small></small> :
<small>
<Markdown source={tagDescription} />
</small>
@@ -108,7 +108,7 @@ export default class Operations extends React.Component {
}
</div>
<button className="expand-operation" title="Expand operation" onClick={() => layoutActions.show(isShownKey, !showTag)}>
<button className="expand-operation" title={showTag ? "Collapse operation": "Expand operation"} onClick={() => layoutActions.show(isShownKey, !showTag)}>
<svg className="arrow" width="20" height="20">
<use href={showTag ? "#large-arrow-down" : "#large-arrow"} xlinkHref={showTag ? "#large-arrow-down" : "#large-arrow"} />
</svg>
+5 -4
View File
@@ -156,19 +156,19 @@ export default class ParameterRow extends Component {
{ (bodyParam || !isExecute) && isDisplayParamItemsEnum ?
<Markdown source={
"<i>Available values</i> : " + paramItemsEnum.map(function(item) {
"<i>Available values</i>: " + paramItemsEnum.map(function(item) {
return item
}).toArray()}/>
}).toArray().join(", ")}/>
: null
}
{ (bodyParam || !isExecute) && paramDefaultValue !== undefined ?
<Markdown source={"<i>Default value</i> : " + paramDefaultValue}/>
<Markdown source={"<i>Default value</i>: " + paramDefaultValue}/>
: null
}
{ (bodyParam || !isExecute) && paramExample !== undefined ?
<Markdown source={"<i>Example</i> : " + paramExample}/>
<Markdown source={"<i>Example</i>: " + paramExample}/>
: null
}
@@ -181,6 +181,7 @@ export default class ParameterRow extends Component {
required={ required }
description={param.get("description") ? `${param.get("name")} - ${param.get("description")}` : `${param.get("name")}`}
onChange={ this.onChangeWrapper }
errors={ param.get("errors") }
schema={ isOAS3 && isOAS3() ? param.get("schema") : param }/>
}
+1 -1
View File
@@ -2,7 +2,7 @@ import React, { Component } from "react"
import PropTypes from "prop-types"
import { getExtensions } from "core/utils"
const propStyle = { color: "#999", fontStyle: "italic" }
const propStyle = { color: "#6b6b6b", fontStyle: "italic" }
export default class Primitive extends Component {
static propTypes = {
+4 -2
View File
@@ -29,10 +29,12 @@ Markdown.propTypes = {
export default Markdown
const sanitizeOptions = {
allowedTags: sanitize.defaults.allowedTags.concat([ "h1", "h2", "img" ]),
allowedTags: sanitize.defaults.allowedTags.concat([ "h1", "h2", "img", "span" ]),
allowedAttributes: {
...sanitize.defaults.allowedAttributes,
"img": sanitize.defaults.allowedAttributes.img.concat(["title"])
"img": sanitize.defaults.allowedAttributes.img.concat(["title"]),
"td": [ "colspan" ],
"*": [ "class" ]
},
textFilter: function(text) {
return text.replace(/&quot;/g, "\"")
+3 -3
View File
@@ -56,13 +56,13 @@ export default class OperationContainer extends PureComponent {
mapStateToProps(nextState, props) {
const { op, layoutSelectors, getConfigs } = props
const { docExpansion, deepLinking, displayOperationId, displayRequestDuration } = getConfigs()
const { docExpansion, deepLinking, displayOperationId, displayRequestDuration, supportedSubmitMethods } = getConfigs()
const showSummary = layoutSelectors.showSummary()
const operationId = op.getIn(["operation", "operationId"]) || op.getIn(["operation", "__originalOperationId"]) || opId(op.get("operation"), props.path, props.method) || op.get("id")
const isShownKey = ["operations", props.tag, operationId]
const isDeepLinkingEnabled = deepLinking && deepLinking !== "false"
const allowTryItOut = typeof props.allowTryItOut === "undefined" ?
props.specSelectors.allowTryItOutFor(props.path, props.method) : props.allowTryItOut
const allowTryItOut = supportedSubmitMethods.indexOf(props.method) >= 0 && (typeof props.allowTryItOut === "undefined" ?
props.specSelectors.allowTryItOutFor(props.path, props.method) : props.allowTryItOut)
const security = op.getIn(["operation", "security"]) || props.specSelectors.security()
return {
+10
View File
@@ -49,6 +49,16 @@ module.exports = function SwaggerUI(opts) {
defaultModelExpandDepth: 1,
defaultModelsExpandDepth: 1,
showExtensions: false,
supportedSubmitMethods: [
"get",
"put",
"post",
"delete",
"options",
"head",
"patch",
"trace"
],
// Initial set of plugins ( TODO rename this, or refactor - we don't need presets _and_ plugins. Its just there for performance.
// Instead, we can compile the first plugin ( it can be a collection of plugins ), then batch the rest.
+21 -15
View File
@@ -1,6 +1,7 @@
import React, { PureComponent, Component } from "react"
import PropTypes from "prop-types"
import { List, fromJS } from "immutable"
import ImPropTypes from "react-immutable-proptypes"
//import "less/json-schema-form"
const noop = ()=> {}
@@ -11,6 +12,7 @@ const JsonSchemaPropShape = {
keyName: PropTypes.any,
fn: PropTypes.object.isRequired,
schema: PropTypes.object,
errors: ImPropTypes.list,
required: PropTypes.bool,
description: PropTypes.any
}
@@ -20,7 +22,8 @@ const JsonSchemaDefaultProps = {
onChange: noop,
schema: {},
keyName: "",
required: false
required: false,
errors: List()
}
export class JsonSchemaForm extends Component {
@@ -29,7 +32,7 @@ export class JsonSchemaForm extends Component {
static defaultProps = JsonSchemaDefaultProps
render() {
let { schema, value, onChange, getComponent, fn } = this.props
let { schema, errors, value, onChange, getComponent, fn } = this.props
if(schema.toJS)
schema = schema.toJS()
@@ -37,7 +40,7 @@ export class JsonSchemaForm extends Component {
let { type, format="" } = schema
let Comp = (format ? getComponent(`JsonSchema_${type}_${format}`) : getComponent(`JsonSchema_${type}`)) || getComponent("JsonSchema_string")
return <Comp { ...this.props } fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema}/>
return <Comp { ...this.props } errors={errors} fn={fn} getComponent={getComponent} value={value} onChange={onChange} schema={schema}/>
}
}
@@ -51,14 +54,15 @@ export class JsonSchema_string extends Component {
}
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, schema, required, description } = this.props
let { getComponent, value, schema, errors, required, description } = this.props
let enumValue = schema["enum"]
let errors = schema.errors || []
errors = errors.toJS ? errors.toJS() : []
if ( enumValue ) {
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
title={ errors.length ? errors : ""}
allowedValues={ enumValue }
value={ value }
allowEmptyValue={ !required }
@@ -70,14 +74,14 @@ export class JsonSchema_string extends Component {
if (schema["type"] === "file") {
return (<Input type="file"
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
title={ errors.length ? errors : ""}
onChange={ this.onChange }
disabled={isDisabled}/>)
}
else {
return (<Input type={ schema.format === "password" ? "password" : "text" }
className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
title={ errors.length ? errors : ""}
value={value}
placeholder={description}
onChange={ this.onChange }
@@ -131,9 +135,10 @@ export class JsonSchema_array extends PureComponent {
}
render() {
let { getComponent, required, schema, fn } = this.props
let { getComponent, required, schema, errors, fn } = this.props
errors = errors.toJS ? errors.toJS() : []
let errors = schema.errors || []
let itemSchema = fn.inferSchema(schema.items)
const JsonSchemaForm = getComponent("JsonSchemaForm")
@@ -145,7 +150,7 @@ export class JsonSchema_array extends PureComponent {
if ( enumValue ) {
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
title={ errors.length ? errors : ""}
multiple={ true }
value={ value }
allowedValues={ enumValue }
@@ -160,7 +165,7 @@ export class JsonSchema_array extends PureComponent {
let schema = Object.assign({}, itemSchema)
if ( errors.length ) {
let err = errors.filter((err) => err.index === i)
if (err.length) schema.errors = [ err[0].error + i ]
if (err.length) errors = [ err[0].error + i ]
}
return (
<div key={i} className="json-schema-form-item">
@@ -182,12 +187,13 @@ export class JsonSchema_boolean extends Component {
onEnumChange = (val) => this.props.onChange(val)
render() {
let { getComponent, value, schema } = this.props
let errors = schema.errors || []
let { getComponent, value, errors, schema } = this.props
errors = errors.toJS ? errors.toJS() : []
const Select = getComponent("Select")
return (<Select className={ errors.length ? "invalid" : ""}
title={ errors.length ? errors : ""}
title={ errors.length ? errors : ""}
value={ String(value) }
allowedValues={ fromJS(schema.enum || ["true", "false"]) }
allowEmptyValue={ !this.props.required }
+2 -2
View File
@@ -52,7 +52,7 @@ export default {
},
[VALIDATE_PARAMS]: ( state, { payload: { pathMethod, isOAS3 } } ) => {
let meta = state.getIn( [ "meta", "paths", ...pathMethod ] )
let meta = state.getIn( [ "meta", "paths", ...pathMethod ], fromJS({}) )
let isXml = /xml/i.test(meta.get("consumes_value"))
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
@@ -68,7 +68,7 @@ export default {
return state.updateIn( [ "resolved", "paths", ...pathMethod, "parameters" ], fromJS([]), parameters => {
return parameters.withMutations( parameters => {
for ( let i = 0, len = parameters.count(); i < len; i++ ) {
parameters.setIn([i, "errors"], fromJS({}))
parameters.setIn([i, "errors"], fromJS([]))
}
})
})
+1 -1
View File
@@ -6,7 +6,7 @@ Plugins allow you to add
- `statePlugins`
- `selectors` - query the state
- `reducers` - modify the state
- `actions` - fire and forget, that will eventually be handled by a reducer. You *can* rely on the result of async actions. But in general its not reccomended
- `actions` - fire and forget, that will eventually be handled by a reducer. You *can* rely on the result of async actions. But in general its not recommended
- `wrapActions` - replace an action with a wrapped action (useful for hooking into existing `actions`)
- `components` - React components
- `fn` - commons functions
+1 -24
View File
@@ -64,35 +64,12 @@
&.execute
{
animation: swagger-ui-pulse 2s infinite;
will-change: transform;
background-color: $btn-execute-background-color;
background-color: $btn-execute-background-color-alt;
color: $btn-execute-font-color;
border-color: $btn-execute-border-color;
}
}
@keyframes swagger-ui-pulse
{
0%
{
color: $btn-execute-font-color;
background: $btn-execute-background-color-alt;
box-shadow: 0 0 0 0 rgba($btn-execute-background-color-alt, .8);
}
70%
{
box-shadow: 0 0 0 5px rgba($btn-execute-background-color-alt, 0);
}
100%
{
color: $btn-execute-font-color;
background: $btn-execute-background-color-alt;
box-shadow: 0 0 0 0 rgba($btn-execute-background-color-alt, 0);
}
}
.btn-group
{
display: flex;
+2 -2
View File
@@ -175,7 +175,7 @@ $section-models-border-color: $gray-custom-2 !default;
$section-models-isopen-h4-border-bottom-color: $section-models-border-color !default;
$section-models-h4-font-color: $gray-500 !default;
$section-models-h4-font-color: $gray-600 !default;
$section-models-h4-background-color-hover: $gray-base !default;
$section-models-h5-font-color: $gray-500 !default;
@@ -188,7 +188,7 @@ $section-models-model-title-font-color: $gray-700 !default;
$prop-type-font-color: #55a !default;
$prop-format-font-color: $gray-300 !default;
$prop-format-font-color: $gray-600 !default;
// Tables
+4 -1
View File
@@ -22,7 +22,7 @@ describe("<LiveResponse/>", function(){
accept: "application/xml",
mutated: "header"
},
url: "http://petstore.swagger.io/v2/pet/1"
url: "http://mutated.petstore.swagger.io/v2/pet/1"
})
let requests = {
@@ -80,6 +80,9 @@ describe("<LiveResponse/>", function(){
expect(curl.length).toEqual(1)
expect(curl.props().request).toBe(requests[test.expected.request])
const expectedUrl = requests[test.expected.request].get("url")
expect(wrapper.find("div.request-url pre").text()).toEqual(expectedUrl)
})
})
})
+12
View File
@@ -7,6 +7,18 @@ import { Markdown as OAS3Markdown } from "corePlugins/oas3/wrap-components/markd
describe("Markdown component", function() {
describe("Swagger 2.0", function() {
it("allows span elements with class attrib", function() {
const str = `<span class="method">ONE</span>`
const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><p><span class="method">ONE</span></p>\n</div>`)
})
it("allows td elements with colspan attrib", function() {
const str = `<table><tr><td>ABC</td></tr></table>`
const el = render(<Markdown source={str} />)
expect(el.html()).toEqual(`<div class="markdown"><table><tr><td>ABC</td></tr></table></div>`)
})
it("allows image elements", function() {
const str = `![Image alt text](http://image.source "Image title")`
const el = render(<Markdown source={str} />)