Repository: speee/jsx-slack
Pull request #66 (2019-10-01 10:53:24)

Add extra types for <Input> block element
Our deprecated dialog support was taking care of [`<input type="hidden" />`](https://github.com/speee/jsx-slack/blob/master/docs/jsx-components-for-dialog.md#input-typehidden-pass-json-state) and [`<input type="submit" />`](https://github.com/speee/jsx-slack/blob/master/docs/jsx-components-for-dialog.md#input-typesubmit-set-the-label-of-submit-button) for more natural writing of template  with HTML form style. And now `<Modal>` has supported them too 😄

```jsx
<Modal callbackId="example" title="Example">
  <input type="hidden" name="foo" value="bar" />
  <input type="hidden" name="userId" value={123} />
  <input type="hidden" name="data" value={[{ hidden: 'value' }]} />

  <input type="text" name="name" label="Name" />
  <input type="submit" value="Send" />
</Modal>
```

```json
{
  "type": "modal",
  "title": {
    "type": "plain_text",
    "text": "Example",
    "emoji": true
  },
  "callback_id": "example",
  "submit": {
    "type": "plain_text",
    "text": "Send",
    "emoji": true
  },
  "private_metadata": "{\"foo\":\"bar\",\"userId\":123,\"data\":[{\"hidden\":\"value\"}]}",
  "blocks": [
    {
      "type": "input",
      "label": {
        "type": "plain_text",
        "text": "Name",
        "emoji": true
      },
      "optional": true,
      "element": {
        "type": "plain_text_input",
        "action_id": "name"
      }
    }
  ]
}
```

[HTML5 types such as `email` and `url`](https://github.com/speee/jsx-slack/blob/master/docs/jsx-components-for-dialog.md#html5-types) are not implemented because Slack has not implemented type validation in Modal.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dade32..bd4222f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,15 +6,17 @@
 
 - [Multi-select menus](https://api.slack.com/reference/block-kit/block-elements#multi_select) ([#56](https://github.com/speee/jsx-slack/issues/56), [#58](https://github.com/speee/jsx-slack/pull/58))
 - `<Modal>` container component ([#60](https://github.com/speee/jsx-slack/pull/60))
-- `<Input>` component for layout block and block element ([#61](https://github.com/speee/jsx-slack/pull/61))
+- `<Input>` layout block and component ([#61](https://github.com/speee/jsx-slack/pull/61))
 - `<Textarea>` component ([#62](https://github.com/speee/jsx-slack/pull/62))
 - Input-compatible props to select-like elements and `<DatePicker>` ([#63](https://github.com/speee/jsx-slack/pull/63))
 - Intrinsic HTML elements of input components ([#65](https://github.com/speee/jsx-slack/pull/65))
+- Add extra types for `<Input>` component ([#66](https://github.com/speee/jsx-slack/pull/66))
 
 ### Changed
 
 - Bump dependent packages to the latest version ([#59](https://github.com/speee/jsx-slack/pull/59))
 - Check invalid elements in `<Blocks>` and `<Input>` strictly ([#64](https://github.com/speee/jsx-slack/pull/64))
+- Split test cases for Block Kit components into multiple files ([#66](https://github.com/speee/jsx-slack/pull/66))
 
 ### Deprecated
 
diff --git a/docs/jsx-components-for-block-kit.md b/docs/jsx-components-for-block-kit.md
index 41efe35..4ab49f3 100644
--- a/docs/jsx-components-for-block-kit.md
+++ b/docs/jsx-components-for-block-kit.md
@@ -48,14 +48,14 @@ api.views.open({
 
 - `title` (**required**): An user-facing title of the modal. (24 characters maximum)
 - `close` (optional): A text for close button of the modal. (24 characters maximum)
-- `submit` (optional): A text for submit button of the modal. (24 characters maximum)
-- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received. (3000 characters maximum)
+- `submit` (optional): A text for submit button of the modal. The value specified in this prop is preferred than [`<Input type="submit">`](#input-typesubmit-set-submit-button-text-of-modal) (24 characters maximum)
+- `privateMetadata` (optional): An optional string that can be found in payloads of some interactive events Slack app received. The value specified in this prop is preferred than [`<Input type="hidden">`](#input-typehidden-store-hidden-values-to-modal). (3000 characters maximum)
 - `clearOnClose` (optional): If enabled by setting `true`, all stacked views will be cleared by close button.
 - `notifyOnClose` (optional): If enabled by setting `true`, `view_closed` event will be sent to request URL of Slack app when closed modal.
 - `callbackId` (optional): An identifier for this modal to recognize it in various events. (255 characters maximum)
 - `externalId` (optional): A unique ID for all views on a per-team basis.
 
-> :information_source: Slack requires the submit text when modal has component for inputs, so jsx-slack would set the text "Submit" as the default value of `submit` prop if you are setting no text together with using input components.
+> :information_source: Slack requires the submit text when modal has component for inputs, so jsx-slack would set the text "Submit" as the default value of `submit` prop if you are setting no submit text in any way together with using input components.
 
 ## Layout blocks
 
@@ -308,11 +308,11 @@ A menu element with static options passed by `<Option>` or `<Optgroup>`. It has
 <Blocks>
   <Actions>
     <Select actionId="rating" placeholder="Rate it!">
-      <Option value="5">5 :star::star::star::star::star:</Option>
-      <Option value="4">4 :star::star::star::star:</Option>
-      <Option value="3">3 :star::star::star:</Option>
-      <Option value="2">2 :star::star:</Option>
-      <Option value="1">1 :star:</Option>
+      <Option value="5">5 {':star:'.repeat(5)}</Option>
+      <Option value="4">4 {':star:'.repeat(4)}</Option>
+      <Option value="3">3 {':star:'.repeat(3)}</Option>
+      <Option value="2">2 {':star:'.repeat(2)}</Option>
+      <Option value="1">1 {':star:'.repeat(1)}</Option>
     </Select>
   </Actions>
 </Blocks>
@@ -370,6 +370,8 @@ In `<Modal>` container, select-like components may place as children of `<Modal>
 </Modal>
 ```
 
+[<img src="https://raw.githubusercontent.com/speee/jsx-slack/master/docs/preview-btn.svg?sanitize=true" width="240" />](https://api.slack.com/tools/block-kit-builder?blocks=%5B%7B%22type%22%3A%22input%22%2C%22hint%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Pick%20language%20you%20want%20to%20learn.%22%2C%22emoji%22%3Atrue%7D%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Language%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22static_select%22%2C%22action_id%22%3A%22language%22%2C%22options%22%3A%5B%7B%22value%22%3A%22javascript%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22JavaScript%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%22python%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Python%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%22java%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Java%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%22c-sharp%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22C%23%22%2C%22emoji%22%3Atrue%7D%7D%2C%7B%22value%22%3A%22php%22%2C%22text%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22PHP%22%2C%22emoji%22%3Atrue%7D%7D%5D%7D%7D%5D&mode=modal)
+
 The above JSX means exactly same as following:
 
 <!-- prettier-ignore-start -->
@@ -645,9 +647,9 @@ As same as [select-like elements](#usage-in-modal-container), `<DatePicker>` als
 
 ### [`<Input>`: Plain-text input element](https://api.slack.com/reference/block-kit/block-elements#input) <a name="input-element" id="input-element">(Only for modal)</a>
 
-`<Input>` block element is for placing a single-line input form within `<Modal>`. It has a interface similar to `<input>` HTML element.
+`<Input>` block element is for placing a single-line input form within `<Modal>`. It can place as children of `<Modal>` directly.
 
-It can place as children of `<Modal>` directly.
+It has a interface similar to `<input>` HTML element and `<input>` intrinsic HTML element also works as well, but the biggest difference is required `label` prop for [`<Input>` layout block](#input-block).
 
 ```jsx
 <Modal title="My App">
@@ -657,15 +659,14 @@ It can place as children of `<Modal>` directly.
 
 [<img src="https://raw.githubusercontent.com/speee/jsx-slack/master/docs/preview-btn.svg?sanitize=true" width="240" />](https://api.slack.com/tools/block-kit-builder?blocks=%5B%7B%22type%22%3A%22input%22%2C%22label%22%3A%7B%22type%22%3A%22plain_text%22%2C%22text%22%3A%22Title%22%2C%22emoji%22%3Atrue%7D%2C%22optional%22%3Afalse%2C%22element%22%3A%7B%22type%22%3A%22plain_text_input%22%2C%22action_id%22%3A%22title%22%2C%22max_length%22%3A80%7D%7D%5D&mode=modal)
 
-`<input>` intrinsic HTML element also works as well.
-
-> :information_source: Internally this is syntactic sugar for [`<Input>` block](#input-block) with [a plain-text input](https://api.slack.com/reference/block-kit/block-elements#input).
+This is same for the other inputable elements too. In children of `<Modal>`, please take care to define `label` prop into them.
 
 #### <a name="input-element-props" id="input-element-props">Props (Block element)</a>
 
 - `label` (**required**): The label string for the element.
-- `id` / `blockId` (optional): A string of unique identifier for [`<Input>` block](#input-block).
+- `id` / `blockId` (optional): A string of unique identifier for [`<Input>` layout block](#input-block).
 - `name` / `actionId` (optional): A string of unique identifier for the action.
+- `type` (optional): `text` by default.
 - `title`/ `hint` (optional): Specify a helpful text appears under the element.
 - `placeholder` (optional): Specify a text string appears within the content of input is empty. (150 characters maximum)
 - `required` (optional): A boolean prop to specify whether any value must be filled when user confirms modal.
@@ -673,6 +674,69 @@ It can place as children of `<Modal>` directly.
 - `maxLength` (optional): The maximum number of characters allowed for the input element. It must up to 3000 character.
 - `minLength` (optional): The minimum number of characters allowed for the input element.
 
+> :information_source: Internally this is syntactic sugar for [`<Input>` layout block](#input-block) with [a plain-text input](https://api.slack.com/reference/block-kit/block-elements#input).
+
+### `<Input type="hidden">`: Store hidden values to modal
+
+By using `<Input type="hidden">`, you can assign hidden values as a private metadata JSON of modal with a familiar way in HTML form.
+
+```jsx
+<Modal title="modal">
+  <Input type="hidden" name="foo" value="bar" />
+  <Input type="hidden" name="userId" value={123} />
+  <Input type="hidden" name="data" value={[{ hidden: 'value' }]} />
+
+  <Input name="name" label="Name" />
+</Modal>
+```
+
+The above example indicates the same modal as following:
+
+```jsx
+<Modal
+  title="modal"
+  privateMetadata={JSON.stringify({
+    foo: 'bar',
+    userId: 123,
+    data: [{ hidden: 'value' }],
+  })}
+>
+  <Input name="name" label="Name" />
+</Modal>
+```
+
+You can use hidden values by parsing JSON stored in [callbacked `private_metadata` from Slack](https://api.slack.com/block-kit/surfaces/modals#private_metadata).
+
+`privateMetadata` prop in parent `<Modal>` _must not define_ when using `<Input type="hidden">`. `<Modal>` prefers `privateMetadata` than `<Input type="hidden">` whenever it has any value in `privateMetadata` prop.
+
+And please take care that the maximum length validation by Slack will still apply for stringified JSON. The value like string and array that cannot predict the length might over the limit of JSON string length easily (3000 characters).
+
+The best practice is only storing the value of a pointer to reference data stored elsewhere. _It's better not to store complex data as hidden value directly._
+
+#### Props
+
+- `type` (**required**): Must be `hidden`.
+- `name` (**required**): The name of hidden value.
+- `value` (**required**): A value to store into modal. It must be [a serializable value to JSON](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#Description).
+
+### `<Input type="submit">`: Set submit button text of modal
+
+`<Input type="submit">` can set the label of submit button for the current modal. It is meaning just an alias into `submit` prop of `<Modal>`, but JSX looks like more natural HTML form.
+
+```jsx
+<Modal title="Example">
+  <Input name="name" label="Name" />
+  <Input type="submit" value="Send" />
+</Modal>
+```
+
+As same as `<Input type="hidden">`, `submit` prop in `<Modal>` must not define when using `<Input type="submit">`. `<Modal>` prefers props defined directly.
+
+#### Props
+
+- `type` (**required**): Must be `submit`.
+- `value` (**required**): A string of submit button for the current modal. (24 characters maximum)
+
 ### `<Textarea>`: Plain-text input element with multiline (Only for modal)
 
 `<Textarea>` component has very similar interface to [`<Input>` block element](#input-element). An only difference is to allow multi-line input.
diff --git a/src/block-kit/Blocks.tsx b/src/block-kit/Blocks.tsx
index 8dca9fd..70580fe 100644
--- a/src/block-kit/Blocks.tsx
+++ b/src/block-kit/Blocks.tsx
@@ -1,6 +1,7 @@
 /** @jsx JSXSlack.h */
 import { JSXSlack, jsxOnParsed } from '../jsx'
 import { ArrayOutput, aliasTo, isNode, wrap } from '../utils'
+import { internalHiddenType, internalSubmitType } from './Input'
 import { Divider, Image, Input, Section, Select, Textarea } from './index'
 
 export interface BlocksProps {
@@ -18,15 +19,22 @@ export enum InternalBlockType {
 
 export const blockTypeSymbol = Symbol('jsx-slack-block-type')
 
-const knownBlocks = [
-  'actions',
-  'context',
-  'divider',
-  'file',
-  'image',
+const knownBlocks: Map<
+  InternalBlockType | undefined,
+  (string | symbol)[]
+> = new Map()
+
+// Blocks
+const base = ['actions', 'context', 'divider', 'file', 'image', 'section']
+knownBlocks.set(undefined, base)
+
+// Modal
+knownBlocks.set(InternalBlockType.modal, [
+  ...base,
   'input',
-  'section',
-]
+  internalHiddenType,
+  internalSubmitType,
+])
 
 export const Blocks: JSXSlack.FC<BlocksProps> = props => {
   const internalType: InternalBlockType | undefined = props[blockTypeSymbol]
@@ -60,12 +68,11 @@ export const Blocks: JSXSlack.FC<BlocksProps> = props => {
 
   node.props[jsxOnParsed] = parsed => {
     // Check the final output
-    if (parsed.some(b => !knownBlocks.includes(b.type)))
-      throw new Error('<Blocks> allows only including layout block components.')
+    const availableBlocks = knownBlocks.get(internalType) || []
 
-    if (internalType === undefined && parsed.some(b => b.type === 'input'))
+    if (parsed.some(b => !availableBlocks.includes(b.type)))
       throw new Error(
-        '<Input> block cannot place in <Blocks> container for messaging.'
+        'Block container allows only including valid layout block components.'
       )
   }
 
diff --git a/src/block-kit/Input.tsx b/src/block-kit/Input.tsx
index a3b83fe..df1d898 100644
--- a/src/block-kit/Input.tsx
+++ b/src/block-kit/Input.tsx
@@ -13,8 +13,11 @@ export interface InputCommonProps extends BlockComponentProps {
   required?: boolean
 }
 
+type InputCommonUndefinedProps = { [key in keyof InputCommonProps]?: undefined }
+
 interface InputBlockProps extends InputCommonProps {
   children: JSXSlack.Node<{}>
+  type?: undefined
 
   // Disallow defining attributes for component usage
   actionId?: undefined
@@ -27,6 +30,7 @@ interface InputBlockProps extends InputCommonProps {
 
 interface InputComponentProps extends InputCommonProps {
   children?: undefined
+  type?: 'text'
 
   actionId?: string // => PlainTextInput.actionId
   name?: string // => PlainTextInput.actionId (Alias)
@@ -36,13 +40,62 @@ interface InputComponentProps extends InputCommonProps {
   minLength?: number // => PlainTextInput.minLength
 }
 
-export type InputProps = InputBlockProps | InputComponentProps
-export type TextareaProps = InputComponentProps
+interface InputHiddenProps extends InputCommonUndefinedProps {
+  children?: undefined
+  type: 'hidden'
+  name: string
+  value: any
+
+  actionId?: undefined
+  placeholder?: undefined
+  maxLength?: undefined
+  minLength?: undefined
+}
+
+interface InputSubmitProps extends InputCommonUndefinedProps {
+  children?: undefined
+  type: 'submit'
+  value: string
+
+  actionId?: undefined
+  name?: undefined
+  placeholder?: undefined
+  maxLength?: undefined
+  minLength?: undefined
+}
+
+type InputProps =
+  | InputBlockProps
+  | InputComponentProps
+  | InputHiddenProps
+  | InputSubmitProps
+
+export type IntrinsicInputProps =
+  | Omit<InputBlockProps, 'actionId' | 'blockId' | 'hint'>
+  | Omit<InputComponentProps, 'actionId' | 'blockId' | 'hint'>
+  | Omit<InputHiddenProps, 'actionId' | 'blockId' | 'hint'>
+  | Omit<InputSubmitProps, 'actionId' | 'blockId' | 'hint'>
+
+export type TextareaProps = Omit<InputComponentProps, 'type'>
 
 export type WithInputProps<T> =
-  | T & { [key in keyof InputCommonProps]?: undefined }
+  | T & InputCommonUndefinedProps
   | T & InputCommonProps
 
+export const internalHiddenType = Symbol('jsx-slack-input-internal-hidden-type')
+export const internalSubmitType = Symbol('jsx-slack-input-internal-submit-type')
+
+export interface InternalHiddenObject {
+  type: typeof internalHiddenType
+  name: string
+  value: any
+}
+
+export interface InternalSubmitObject {
+  type: typeof internalSubmitType
+  value: string
+}
+
 const knownInputs = [
   'channels_select',
   'conversations_select',
@@ -89,7 +142,27 @@ const InputComponent: JSXSlack.FC<
   )
 
 export const Input: JSXSlack.FC<InputProps> = props => {
-  if (props.children === undefined) return InputComponent(props)
+  if (props.children === undefined) {
+    switch (props.type) {
+      case 'hidden':
+        return (
+          <ObjectOutput<InternalHiddenObject>
+            type={internalHiddenType}
+            name={props.name}
+            value={props.value}
+          />
+        )
+      case 'submit':
+        return (
+          <ObjectOutput<InternalSubmitObject>
+            type={internalSubmitType}
+            value={props.value}
+          />
+        )
+      default:
+        return InputComponent(props)
+    }
+  }
 
   const hintText = props.hint || props.title
   const node = (
diff --git a/src/block-kit/Modal.tsx b/src/block-kit/Modal.tsx
index 413bc9c..d551203 100644
--- a/src/block-kit/Modal.tsx
+++ b/src/block-kit/Modal.tsx
@@ -8,6 +8,7 @@ import {
   blockTypeSymbol,
   InternalBlockType,
 } from './Blocks'
+import { internalHiddenType, internalSubmitType } from './Input'
 import { plainText } from './composition/utils'
 
 // TODO: Use original View type when supported fields for API on @slack/types
@@ -31,17 +32,49 @@ export interface ModalProps {
 const submitText = plainText('Submit')
 
 export const Modal: JSXSlack.FC<ModalProps> = props => {
+  let hasInputBlock = false
+  let hidden: any
+  let inputSubmit: string | undefined
+
   const blocks = JSXSlack(
     <BlocksInternal
       {...{ [blockTypeSymbol]: InternalBlockType.modal }}
       children={props.children}
     />
-  )
+  ).reduce((arr, block) => {
+    switch (block.type) {
+      case internalSubmitType:
+        inputSubmit = block.value
+        break
+      case internalHiddenType:
+        hidden = hidden || {}
+        hidden[block.name] = block.value
+        break
+      default:
+        if (block.type === 'input') hasInputBlock = true
+        return [...arr, block]
+    }
+    return arr
+  }, [])
+
+  const submit = (() => {
+    if (props.submit) return plainText(props.submit)
+    if (inputSubmit) return plainText(inputSubmit)
+
+    // "submit" field is required when using input block
+    if (hasInputBlock) return submitText
+
+    return undefined
+  })()
+
+  const privateMetadata = (() => {
+    if (props.privateMetadata !== undefined)
+      return props.privateMetadata.toString()
+
+    if (hidden) return JSON.stringify(hidden)
 
-  // "submit" field is required when using input block
-  const defaultSubmit = blocks.some(b => b.type === 'input')
-    ? submitText
-    : undefined
+    return undefined
+  })()
 
   return (
     <ObjectOutput<ViewForAPI>
@@ -49,9 +82,9 @@ export const Modal: JSXSlack.FC<ModalProps> = props => {
       title={plainText(props.title)}
       callback_id={props.callbackId}
       external_id={props.externalId}
-      submit={props.submit ? plainText(props.submit) : defaultSubmit}
+      submit={submit}
       close={props.close ? plainText(props.close) : undefined}
-      private_metadata={props.privateMetadata}
+      private_metadata={privateMetadata}
       clear_on_close={
         props.clearOnClose !== undefined ? props.clearOnClose : undefined
       }
diff --git a/src/jsx.ts b/src/jsx.ts
index 20d2dc2..88ba0d7 100644
--- a/src/jsx.ts
+++ b/src/jsx.ts
@@ -3,7 +3,7 @@ import flattenDeep from 'lodash.flattendeep'
 import { escapeChars, escapeEntity, parse } from './html'
 import turndown from './turndown'
 import { wrap } from './utils'
-import { InputProps, TextareaProps } from './block-kit/Input'
+import { IntrinsicInputProps, TextareaProps } from './block-kit/Input'
 import { ButtonProps } from './block-kit/elements/Button'
 import {
   SelectProps,
@@ -172,7 +172,7 @@ export namespace JSXSlack {
       hr: { id?: string }
       i: {}
       img: { alt: string; id?: string; src: string; title?: string }
-      input: Omit<InputProps, 'actionId' | 'blockId' | 'hint'>
+      input: IntrinsicInputProps
       li: {}
       ol: { start?: number; children: Children<any> }
       optgroup: OptgroupProps


Test patch:
diff --git a/test/block-kit/block-elements/composition-objects.tsx b/test/block-kit/block-elements/composition-objects.tsx
new file mode 100644
index 0000000..d40a48c
--- /dev/null
+++ b/test/block-kit/block-elements/composition-objects.tsx
@@ -0,0 +1,70 @@
+/** @jsx JSXSlack.h */
+import JSXSlack, { Actions, Blocks, Button, Confirm } from '../../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Composition objects', () => {
+  describe('<Confirm>', () => {
+    it('outputs action included <Confirm> object', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Button
+                confirm={
+                  <Confirm
+                    title="Share to SNS"
+                    confirm="Yes, please"
+                    deny="Cancel"
+                  >
+                    <b>Are you sure?</b> Message will be share.
+                  </Confirm>
+                }
+              >
+                Share
+              </Button>
+            </Actions>
+          </Blocks>
+        )
+      ).toMatchInlineSnapshot(`
+        Array [
+          Object {
+            "block_id": "actions",
+            "elements": Array [
+              Object {
+                "confirm": Object {
+                  "confirm": Object {
+                    "emoji": true,
+                    "text": "Yes, please",
+                    "type": "plain_text",
+                  },
+                  "deny": Object {
+                    "emoji": true,
+                    "text": "Cancel",
+                    "type": "plain_text",
+                  },
+                  "text": Object {
+                    "text": "*Are you sure?* Message will be share.",
+                    "type": "mrkdwn",
+                    "verbatim": true,
+                  },
+                  "title": Object {
+                    "emoji": true,
+                    "text": "Share to SNS",
+                    "type": "plain_text",
+                  },
+                },
+                "text": Object {
+                  "emoji": true,
+                  "text": "Share",
+                  "type": "plain_text",
+                },
+                "type": "button",
+              },
+            ],
+            "type": "actions",
+          },
+        ]
+      `))
+  })
+})
diff --git a/test/block-kit/block-elements/input-components-for-modal.tsx b/test/block-kit/block-elements/input-components-for-modal.tsx
new file mode 100644
index 0000000..48e978f
--- /dev/null
+++ b/test/block-kit/block-elements/input-components-for-modal.tsx
@@ -0,0 +1,198 @@
+/** @jsx JSXSlack.h */
+import { InputBlock, View } from '@slack/types'
+import JSXSlack, {
+  ChannelsSelect,
+  ConversationsSelect,
+  DatePicker,
+  ExternalSelect,
+  Input,
+  Modal,
+  Option,
+  Select,
+  Textarea,
+  UsersSelect,
+} from '../../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Input components for modal', () => {
+  it('wraps input-compatible block elements in <Input> block when passed label prop', () => {
+    for (const Compatible of [
+      props => (
+        <Select {...props}>
+          <Option value="test">test</Option>
+        </Select>
+      ),
+      ExternalSelect,
+      ChannelsSelect,
+      ConversationsSelect,
+      UsersSelect,
+      DatePicker,
+    ]) {
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <Compatible id="test" label="Test" name="name" title="foobar" />
+          </Modal>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Modal title="test">
+            <Input blockId="test" label="Test" hint="foobar">
+              <Compatible actionId="name" />
+            </Input>
+          </Modal>
+        )
+      )
+    }
+  })
+
+  describe('<Input> (component)', () => {
+    const expected: InputBlock = {
+      type: 'input',
+      block_id: 'foo',
+      label: { type: 'plain_text', text: 'Input', emoji: true },
+      hint: { type: 'plain_text', text: 'bar', emoji: true },
+      optional: true,
+      element: {
+        type: 'plain_text_input',
+        action_id: 'action',
+      },
+    }
+
+    it('outputs input block with plain-text input element', () =>
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <Input actionId="action" blockId="foo" hint="bar" label="Input" />
+          </Modal>
+        ).blocks
+      ).toStrictEqual([expected]))
+
+    it('allows using HTML-compatible <input> element', () =>
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <input
+              type="text"
+              id="foo"
+              label="Input"
+              name="action"
+              title="bar"
+            />
+          </Modal>
+        ).blocks
+      ).toStrictEqual([expected]))
+
+    it("marks placeholder's emoji flag as disabled", () => {
+      const { blocks: blocksPlaceholder } = JSXSlack(
+        <Modal title="test">
+          <Input label="placeholder" placeholder="Hi 😃" />
+        </Modal>
+      )
+
+      expect(blocksPlaceholder[0].element.placeholder).toStrictEqual({
+        type: 'plain_text',
+        text: 'Hi 😃',
+        emoji: false,
+      })
+    })
+  })
+
+  describe('<Input type="hidden">', () => {
+    it('assigns private metadata of parent modal as JSON string', () => {
+      const modal: View = JSXSlack(
+        <Modal title="test">
+          <Input type="text" name="a" label="a" />
+
+          <Input type="hidden" name="string" value="foobar" />
+          <Input type="hidden" name="number" value={123} />
+          <Input type="hidden" name="array" value={[1, 2, 3]} />
+          <Input type="hidden" name="object" value={{ test: null }} />
+          <Input type="hidden" name="boolean" value={true} />
+        </Modal>
+      )
+
+      expect(JSON.parse(modal.private_metadata || '')).toStrictEqual({
+        string: 'foobar',
+        number: 123,
+        array: [1, 2, 3],
+        object: { test: null },
+        boolean: true,
+      })
+    })
+
+    it('prefers privateMetadata prop of <Modal>', () => {
+      const modal: View = JSXSlack(
+        <Modal title="test" privateMetadata="customMeta">
+          <Input type="text" name="a" label="a" />
+          <Input type="hidden" name="foo" value="bar" />
+        </Modal>
+      )
+
+      expect(modal.private_metadata).toBe('customMeta')
+    })
+  })
+
+  describe('<Input type="submit">', () => {
+    it('assigns the label of submit to the parent modal', () => {
+      const modal: View = JSXSlack(
+        <Modal title="test">
+          <Input name="a" label="a" />
+          <Input type="submit" value="Send!" />
+        </Modal>
+      )
+
+      expect(modal.submit).toStrictEqual({
+        type: 'plain_text',
+        text: 'Send!',
+        emoji: true,
+      })
+    })
+
+    it('prefers submit prop of <Modal>', () => {
+      const modal: View = JSXSlack(
+        <Modal title="test" submit="Submit label">
+          <Input name="a" label="a" />
+          <Input type="submit" value="Send!" />
+        </Modal>
+      )
+
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      expect(modal.submit!.text).toBe('Submit label')
+    })
+  })
+
+  describe('<Textarea>', () => {
+    const expected: InputBlock = {
+      type: 'input',
+      label: { type: 'plain_text', text: 'textarea', emoji: true },
+      optional: true,
+      element: {
+        type: 'plain_text_input',
+        action_id: 'foobar',
+        multiline: true,
+      },
+    }
+
+    it('outputs input block with plain-text input element that is enabled multiline prop', () => {
+      const { blocks } = JSXSlack(
+        <Modal title="test">
+          <Textarea label="textarea" name="foobar" />
+        </Modal>
+      )
+
+      expect(blocks).toStrictEqual([expected])
+    })
+
+    it('allows using HTML-compatible <textarea> element', () => {
+      const { blocks } = JSXSlack(
+        <Modal title="test">
+          <textarea label="textarea" name="foobar" />
+        </Modal>
+      )
+
+      expect(blocks).toStrictEqual([expected])
+    })
+  })
+})
diff --git a/test/block-kit/block-elements/interactive-components.tsx b/test/block-kit/block-elements/interactive-components.tsx
new file mode 100644
index 0000000..c5a1437
--- /dev/null
+++ b/test/block-kit/block-elements/interactive-components.tsx
@@ -0,0 +1,646 @@
+/** @jsx JSXSlack.h */
+import {
+  ActionsBlock,
+  Option as SlackOption,
+  Overflow as SlackOverflow,
+  SectionBlock,
+} from '@slack/types'
+import JSXSlack, {
+  Actions,
+  Blocks,
+  Button,
+  ChannelsSelect,
+  Confirm,
+  ConversationsSelect,
+  DatePicker,
+  ExternalSelect,
+  Optgroup,
+  Option,
+  Overflow,
+  OverflowItem,
+  Section,
+  Select,
+  UsersSelect,
+} from '../../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Interactive components', () => {
+  const action = (...elements: ActionsBlock['elements']): ActionsBlock => ({
+    block_id: 'actions',
+    elements,
+    type: 'actions',
+  })
+
+  describe('<Button>', () => {
+    it('outputs button in actions block', () => {
+      const buttonAction = action({
+        type: 'button',
+        action_id: 'action',
+        text: { type: 'plain_text', text: 'Hello!', emoji: true },
+        value: 'value',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Button actionId="action" value="value">
+                Hello!
+              </Button>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([buttonAction])
+    })
+
+    it('allows using HTML-compatible <button> element', () => {
+      const buttonAction = action({
+        type: 'button',
+        action_id: 'action',
+        text: { type: 'plain_text', text: 'Hello!', emoji: true },
+        value: 'value',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <button name="action" value="value">
+                Hello!
+              </button>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([buttonAction])
+    })
+
+    it('outputs button for link in actions block', () => {
+      const buttonAction = action({
+        type: 'button',
+        url: 'https://example.com/',
+        text: { type: 'plain_text', text: 'Link Button', emoji: true },
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Button url="https://example.com/">Link Button</Button>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([buttonAction])
+    })
+
+    it('outputs styled button in actions block', () => {
+      const buttonAction = action(
+        {
+          type: 'button',
+          text: { type: 'plain_text', text: 'Default', emoji: true },
+        },
+        {
+          type: 'button',
+          text: { type: 'plain_text', text: 'Primary', emoji: true },
+          style: 'primary',
+        },
+        {
+          type: 'button',
+          text: { type: 'plain_text', text: 'Danger', emoji: true },
+          style: 'danger',
+        }
+      )
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Button>Default</Button>
+              <Button style="primary">Primary</Button>
+              <Button style="danger">Danger</Button>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([buttonAction])
+    })
+  })
+
+  describe('<Select>', () => {
+    it('outputs select menu for static items in actions block', () => {
+      const selectAction = action({
+        type: 'static_select',
+        action_id: 'select',
+        placeholder: { type: 'plain_text', text: 'Select box', emoji: true },
+        options: [
+          {
+            text: { type: 'plain_text', text: '1st', emoji: true },
+            value: 'first',
+          },
+          {
+            text: { type: 'plain_text', text: '2nd', emoji: true },
+            value: 'second',
+          },
+          {
+            text: { type: 'plain_text', text: '3rd', emoji: true },
+            value: 'third',
+          },
+        ],
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Select actionId="select" placeholder="Select box">
+                <Option value="first">1st</Option>
+                <Option value="second">2nd</Option>
+                <Option value="third">3rd</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+
+    it('outputs multiple select menu for static items in section block', () => {
+      const [section]: SectionBlock[] = JSXSlack(
+        <Blocks>
+          <Section>
+            test
+            <Select multiple>
+              <Option value="a">a</Option>
+              <Option value="b">b</Option>
+              <Option value="c">c</Option>
+            </Select>
+          </Section>
+        </Blocks>
+      )
+
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      expect(section.accessory!.type).toBe('multi_static_select')
+    })
+
+    it('outputs select menu with grouped items in actions block', () => {
+      const selectAction = action({
+        type: 'static_select',
+        action_id: 'select',
+        placeholder: { type: 'plain_text', text: 'Group', emoji: true },
+        option_groups: [
+          {
+            label: { type: 'plain_text', text: 'A', emoji: true },
+            options: [
+              {
+                text: { type: 'plain_text', text: '1st', emoji: true },
+                value: 'first',
+              },
+              {
+                text: { type: 'plain_text', text: '2nd', emoji: true },
+                value: 'second',
+              },
+            ],
+          },
+          {
+            label: { type: 'plain_text', text: 'B', emoji: true },
+            options: [
+              {
+                text: { type: 'plain_text', text: '3rd', emoji: true },
+                value: 'third',
+              },
+              {
+                text: { type: 'plain_text', text: '4th', emoji: true },
+                value: 'fourth',
+              },
+            ],
+          },
+        ],
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Select actionId="select" placeholder="Group">
+                <Optgroup label="A">
+                  <Option value="first">1st</Option>
+                  <Option value="second">2nd</Option>
+                </Optgroup>
+                <Optgroup label="B">
+                  <Option value="third">3rd</Option>
+                  <Option value="fourth">4th</Option>
+                </Optgroup>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+
+    it('outputs select menu with initial option in actions block', () => {
+      const selectAction = action({
+        type: 'static_select',
+        action_id: 'select',
+        options: [
+          {
+            text: { type: 'plain_text', text: '1st', emoji: true },
+            value: 'first',
+          },
+          {
+            text: { type: 'plain_text', text: '2nd', emoji: true },
+            value: 'second',
+          },
+          {
+            text: { type: 'plain_text', text: '3rd', emoji: true },
+            value: 'third',
+          },
+        ],
+        initial_option: {
+          text: { type: 'plain_text', text: '3rd', emoji: true },
+          value: 'third',
+        },
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Select actionId="select" value="third">
+                <Option value="first">1st</Option>
+                <Option value="second">2nd</Option>
+                <Option value="third">3rd</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+
+    it('ignores invalid strings in children of select', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                Ignores invalid string
+                <Option value="a">a</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                <Option value="a">a</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ))
+
+    it('allows using HTML-compatible <select>, <option> and <optgroup> elements', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <select name="test">
+                <optgroup label="foo">
+                  <option value="bar">bar</option>
+                </optgroup>
+              </select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select actionId="test">
+                <Optgroup label="foo">
+                  <Option value="bar">bar</Option>
+                </Optgroup>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ))
+
+    it('throws error when passed multiple select in actions block', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select multiple>
+                <Option value="error">error</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow())
+
+    it('throws error when <Select> has not contained <Option>', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>{}</Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow(/must include/i))
+
+    it('throws error when <Select> has contained invalid block', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                <ExternalSelect />
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow(/unexpected option type/i))
+
+    it('throws error when <Select> has mixed children', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                <Option value="first">1st</Option>
+                <Optgroup label="A">
+                  <Option value="second">2nd</Option>
+                  <Option value="third">3rd</Option>
+                </Optgroup>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow(/only include either of/))
+  })
+
+  describe('<ExternalSelect>', () => {
+    it('outputs select menu for external items in actions block', () => {
+      const initialOption: SlackOption = {
+        text: { type: 'plain_text', text: 'value', emoji: false },
+        value: 'value',
+      }
+
+      const selectAction = action({
+        type: 'external_select',
+        action_id: 'external',
+        placeholder: { type: 'plain_text', text: 'External', emoji: true },
+        min_query_length: 4,
+        initial_option: initialOption,
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <ExternalSelect
+                actionId="external"
+                placeholder="External"
+                minQueryLength={4}
+                initialOption={initialOption}
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+
+    it('outputs select for external items with initial option defined by <Option> in actions block', () => {
+      const selectAction = action({
+        type: 'external_select',
+        initial_option: {
+          text: { type: 'plain_text', text: 'Option value', emoji: true },
+          value: 'option',
+        },
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <ExternalSelect
+                initialOption={<Option value="option">Option value</Option>}
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+  })
+
+  describe('<UsersSelect>', () => {
+    it('outputs select for users in actions block', () => {
+      const selectAction = action({
+        type: 'users_select',
+        action_id: 'users',
+        placeholder: { type: 'plain_text', text: 'Select user', emoji: true },
+        initial_user: 'U01234567',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <UsersSelect
+                actionId="users"
+                placeholder="Select user"
+                initialUser="U01234567"
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+  })
+
+  describe('<ConversationsSelect>', () => {
+    it('outputs select for conversations in actions block', () => {
+      const selectAction = action({
+        type: 'conversations_select',
+        action_id: 'conversations',
+        placeholder: {
+          type: 'plain_text',
+          text: 'Select conversation',
+          emoji: true,
+        },
+        initial_conversation: 'C89ABCDEF',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <ConversationsSelect
+                actionId="conversations"
+                placeholder="Select conversation"
+                initialConversation="C89ABCDEF"
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+  })
+
+  describe('<ChannelsSelect>', () => {
+    it('outputs select for channels in actions block', () => {
+      const selectAction = action({
+        type: 'channels_select',
+        action_id: 'channels',
+        placeholder: {
+          type: 'plain_text',
+          text: 'Select channel',
+          emoji: true,
+        },
+        initial_channel: 'C98765432',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <ChannelsSelect
+                actionId="channels"
+                placeholder="Select channel"
+                initialChannel="C98765432"
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([selectAction])
+    })
+  })
+
+  describe('<Overflow>', () => {
+    it('outputs overflow menu in actions block', () => {
+      const baseOverflow: SlackOverflow = {
+        type: 'overflow',
+        action_id: 'overflow_menu',
+        options: [
+          {
+            text: { type: 'plain_text', text: 'Menu A', emoji: true },
+            value: 'menu_a',
+          },
+          {
+            text: { type: 'plain_text', text: 'Menu B', emoji: true },
+            value: 'menu_b',
+          },
+          {
+            text: { type: 'plain_text', text: 'Menu C', emoji: true },
+            value: 'menu_c',
+          },
+          {
+            text: { type: 'plain_text', text: 'Link', emoji: true },
+            url: 'https://example.com/',
+          },
+        ],
+      }
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <Overflow actionId="overflow_menu">
+                <OverflowItem value="menu_a">Menu A</OverflowItem>
+                <OverflowItem value="menu_b">Menu B</OverflowItem>
+                <OverflowItem value="menu_c">Menu C</OverflowItem>
+                <OverflowItem url="https://example.com/">Link</OverflowItem>
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([action(baseOverflow)])
+
+      // confirm prop and HTML-compatible props
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions id="actions">
+              <Overflow
+                name="overflow_menu"
+                confirm={
+                  <Confirm title="a" confirm="b" deny="c">
+                    foobar
+                  </Confirm>
+                }
+              >
+                <OverflowItem value="menu_a">Menu A</OverflowItem>
+                <OverflowItem value="menu_b">Menu B</OverflowItem>
+                <OverflowItem value="menu_c">Menu C</OverflowItem>
+                <OverflowItem url="https://example.com/">Link</OverflowItem>
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([
+        action({
+          ...baseOverflow,
+          confirm: {
+            title: { type: 'plain_text', text: 'a', emoji: true },
+            confirm: { type: 'plain_text', text: 'b', emoji: true },
+            deny: { type: 'plain_text', text: 'c', emoji: true },
+            text: { type: 'mrkdwn', text: 'foobar', verbatim: true },
+          },
+        }),
+      ])
+    })
+
+    it('throws error when <Overflow> has only one <OverflowItem>', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Overflow>
+                <OverflowItem value="a">A</OverflowItem>
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow())
+
+    it('throws error when <Overflow> has unexpected children', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Overflow>
+                <Button>btn</Button>
+                <Button>btn</Button>
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow())
+  })
+
+  describe('<DatePicker>', () => {
+    it('outputs date picker in actions block', () => {
+      const datePickerAction = action({
+        type: 'datepicker',
+        action_id: 'date_picker',
+        placeholder: { type: 'plain_text', text: 'Select date', emoji: true },
+        initial_date: '2019-01-23',
+      })
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions blockId="actions">
+              <DatePicker
+                actionId="date_picker"
+                placeholder="Select date"
+                initialDate={new Date(1548214496000)} // 2019-01-23 12:34:56
+              />
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual([datePickerAction])
+    })
+  })
+})
diff --git a/test/block-kit/builtin-components.tsx b/test/block-kit/builtin-components.tsx
new file mode 100644
index 0000000..94cbad2
--- /dev/null
+++ b/test/block-kit/builtin-components.tsx
@@ -0,0 +1,343 @@
+/** @jsx JSXSlack.h */
+import { StaticSelect } from '@slack/types'
+import JSXSlack, {
+  Actions,
+  Blocks,
+  Divider,
+  Escape,
+  Fragment,
+  Optgroup,
+  Option,
+  Overflow,
+  OverflowItem,
+  Section,
+  Select,
+  SelectFragment,
+} from '../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Built-in components', () => {
+  describe('<Escape> component', () => {
+    it('replaces special character in wrapped by <Escape> component', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Section>&gt; *bold* _italic_ ~strikethrough~ `code`</Section>
+            <Section>
+              <Escape>&gt; *bold* _italic_ ~strikethrough~ `code`</Escape>
+            </Section>
+          </Blocks>
+        )
+      ).toStrictEqual([
+        expect.objectContaining({
+          text: expect.objectContaining({
+            text: '&gt; *bold* _italic_ ~strikethrough~ `code`',
+          }),
+        }),
+        expect.objectContaining({
+          text: expect.objectContaining({
+            text:
+              '\u00ad&gt; \u2217bold\u2217 \u02cditalic\u02cd \u223cstrikethrough\u223c \u02cbcode\u02cb',
+          }),
+        }),
+      ]))
+  })
+
+  describe('<Fragment> component', () => {
+    it('allows grouping multiple components for custom block', () => {
+      const CustomBlock: JSXSlack.FC<{ children: JSXSlack.Children<{}> }> = ({
+        children,
+      }) => (
+        <Fragment>
+          <Divider />
+          <Section>{children}</Section>
+          <Divider />
+        </Fragment>
+      )
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <CustomBlock>Hello!</CustomBlock>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Divider />
+            <Section>Hello!</Section>
+            <Divider />
+          </Blocks>
+        )
+      )
+    })
+
+    it('allows grouping select options', () => {
+      const CustomOptions: JSXSlack.FC = () => (
+        <Fragment>
+          <Option value="a">A</Option>
+          <Option value="b">B</Option>
+        </Fragment>
+      )
+
+      // for <Select>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                <CustomOptions />
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Select>
+                <Option value="a">A</Option>
+                <Option value="b">B</Option>
+              </Select>
+            </Actions>
+          </Blocks>
+        )
+      )
+
+      // for <SelectFragment>
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <CustomOptions />
+          </SelectFragment>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <SelectFragment>
+            <Option value="a">A</Option>
+            <Option value="b">B</Option>
+          </SelectFragment>
+        )
+      )
+    })
+
+    it('allows grouping overflow items', () => {
+      const CustomOverflowItems: JSXSlack.FC = () => (
+        <Fragment>
+          <OverflowItem value="a">A</OverflowItem>
+          <OverflowItem value="b">B</OverflowItem>
+        </Fragment>
+      )
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Overflow>
+                <CustomOverflowItems />
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              <Overflow>
+                <OverflowItem value="a">A</OverflowItem>
+                <OverflowItem value="b">B</OverflowItem>
+              </Overflow>
+            </Actions>
+          </Blocks>
+        )
+      )
+    })
+
+    it('allows grouping multiple HTML elements', () => {
+      const CustomList: JSXSlack.FC = () => (
+        <Fragment>
+          <li>A</li>
+          <li>B</li>
+        </Fragment>
+      )
+
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Section>
+              <ul>
+                <CustomList />
+              </ul>
+            </Section>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Section>
+              <ul>
+                <li>A</li>
+                <li>B</li>
+              </ul>
+            </Section>
+          </Blocks>
+        )
+      )
+    })
+
+    it('allows nested fragments', () => {
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Fragment>
+              <Fragment>
+                <Section>Nested</Section>
+                <Section>fragments</Section>
+              </Fragment>
+              <Fragment>
+                <Section>with</Section>
+                <Section>multiple</Section>
+              </Fragment>
+            </Fragment>
+            <Fragment>
+              <Fragment>
+                <Section>components</Section>
+                <Section>are</Section>
+              </Fragment>
+              <Fragment>
+                <Section>supported</Section>
+                <Section>well</Section>
+              </Fragment>
+            </Fragment>
+          </Blocks>
+        )
+      ).toStrictEqual(
+        JSXSlack(
+          <Blocks>
+            <Section>Nested</Section>
+            <Section>fragments</Section>
+            <Section>with</Section>
+            <Section>multiple</Section>
+            <Section>components</Section>
+            <Section>are</Section>
+            <Section>supported</Section>
+            <Section>well</Section>
+          </Blocks>
+        )
+      )
+    })
+  })
+
+  describe('<SelectFragment> component', () => {
+    it('allows building object for external data source of <ExternalSelect>', () => {
+      const expectedOptions: Required<Pick<StaticSelect, 'options'>> = {
+        options: [
+          {
+            text: { type: 'plain_text', text: 'A', emoji: true },
+            value: 'a',
+          },
+          {
+            text: { type: 'plain_text', text: 'B', emoji: true },
+            value: 'b',
+          },
+          {
+            text: { type: 'plain_text', text: 'C', emoji: true },
+            value: 'c',
+          },
+        ],
+      }
+
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <Option value="a">A</Option>
+            <Option value="b">B</Option>
+            <Option value="c">C</Option>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptions)
+
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <option value="a">A</option>
+            <option value="b">B</option>
+            <option value="c">C</option>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptions)
+
+      const expectedOptgroups: Required<Pick<StaticSelect, 'option_groups'>> = {
+        option_groups: [
+          {
+            label: { type: 'plain_text', text: 'A', emoji: true },
+            options: [
+              {
+                text: { type: 'plain_text', text: 'one', emoji: true },
+                value: '1',
+              },
+              {
+                text: { type: 'plain_text', text: 'two', emoji: true },
+                value: '2',
+              },
+            ],
+          },
+          {
+            label: { type: 'plain_text', text: 'B', emoji: true },
+            options: [
+              {
+                text: { type: 'plain_text', text: 'three', emoji: true },
+                value: '3',
+              },
+              {
+                text: { type: 'plain_text', text: 'four', emoji: true },
+                value: '4',
+              },
+            ],
+          },
+        ],
+      }
+
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <Optgroup label="A">
+              <Option value="1">one</Option>
+              <Option value="2">two</Option>
+            </Optgroup>
+            <Optgroup label="B">
+              <Option value="3">three</Option>
+              <Option value="4">four</Option>
+            </Optgroup>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptgroups)
+
+      expect(
+        JSXSlack(
+          <SelectFragment>
+            <optgroup label="A">
+              <option value="1">one</option>
+              <option value="2">two</option>
+            </optgroup>
+            <optgroup label="B">
+              <option value="3">three</option>
+              <option value="4">four</option>
+            </optgroup>
+          </SelectFragment>
+        )
+      ).toStrictEqual(expectedOptgroups)
+    })
+
+    it('allows no options to return empty result', () => {
+      expect(JSXSlack(<SelectFragment />)).toStrictEqual({
+        options: [],
+      })
+
+      expect(JSXSlack(<SelectFragment>{}</SelectFragment>)).toStrictEqual({
+        options: [],
+      })
+    })
+  })
+})
diff --git a/test/block-kit/container-components.tsx b/test/block-kit/container-components.tsx
new file mode 100644
index 0000000..4585f50
--- /dev/null
+++ b/test/block-kit/container-components.tsx
@@ -0,0 +1,99 @@
+/** @jsx JSXSlack.h */
+import { View } from '@slack/types'
+import JSXSlack, {
+  Blocks,
+  Escape,
+  Input,
+  Modal,
+  Option,
+  Section,
+  Select,
+} from '../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Container components', () => {
+  describe('<Blocks>', () => {
+    it('throws error when <Blocks> has unexpected element', () => {
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <b>unexpected</b>
+          </Blocks>
+        )
+      ).toThrow()
+
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Escape>unexpected</Escape>
+          </Blocks>
+        )
+      ).toThrow()
+
+      // <Input> block cannot use in message
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Input label="Select">
+              <Select>
+                <Option value="test">test</Option>
+              </Select>
+            </Input>
+          </Blocks>
+        )
+      ).toThrow()
+    })
+  })
+
+  describe('<Modal>', () => {
+    it('generates view payload JSON', () => {
+      const simpleView: View = {
+        type: 'modal',
+        title: { type: 'plain_text', text: 'test', emoji: true },
+        blocks: [{ type: 'section', text: expect.any(Object) }],
+      }
+
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <Section>Hello!</Section>
+          </Modal>
+        )
+      ).toStrictEqual(simpleView)
+
+      // Optional attributes
+      const viewWithOptions: View & Record<string, any> = {
+        type: 'modal',
+        title: expect.any(Object),
+        blocks: expect.any(Array),
+        submit: { type: 'plain_text', text: 'Submit', emoji: true },
+        close: { type: 'plain_text', text: 'Close', emoji: true },
+        private_metadata: 'private_metadata',
+        clear_on_close: true,
+        notify_on_close: false,
+
+        // Fields for API
+        callback_id: 'callback_id',
+        external_id: 'external_id',
+      }
+
+      expect(
+        JSXSlack(
+          <Modal
+            callbackId="callback_id"
+            clearOnClose
+            close="Close"
+            externalId="external_id"
+            notifyOnClose={false}
+            privateMetadata="private_metadata"
+            submit="Submit"
+            title="test"
+          >
+            <Section>Hello!</Section>
+          </Modal>
+        )
+      ).toStrictEqual(viewWithOptions)
+    })
+  })
+})
diff --git a/test/block-kit/layout-blocks.tsx b/test/block-kit/layout-blocks.tsx
new file mode 100644
index 0000000..8cd3e9a
--- /dev/null
+++ b/test/block-kit/layout-blocks.tsx
@@ -0,0 +1,538 @@
+/** @jsx JSXSlack.h */
+import {
+  DividerBlock,
+  FileBlock,
+  ImageBlock,
+  InputBlock,
+  SectionBlock,
+} from '@slack/types'
+import JSXSlack, {
+  Actions,
+  Blocks,
+  Button,
+  ChannelsSelect,
+  Context,
+  ConversationsSelect,
+  DatePicker,
+  Divider,
+  ExternalSelect,
+  Field,
+  File,
+  Image,
+  Input,
+  Modal,
+  Option,
+  Overflow,
+  OverflowItem,
+  Section,
+  Select,
+  UsersSelect,
+} from '../../src/index'
+
+beforeEach(() => JSXSlack.exactMode(false))
+
+describe('Layout blocks', () => {
+  describe('<Section>', () => {
+    const section: SectionBlock = {
+      type: 'section',
+      block_id: 'hello',
+      text: { type: 'mrkdwn', text: 'Hello!', verbatim: true },
+    }
+
+    it('outputs section block', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Section blockId="hello">Hello!</Section>
+          </Blocks>
+        )
+      ).toStrictEqual([section]))
+
+    it('allows using HTML-compatible <section> element', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <section id="hello">Hello!</section>
+          </Blocks>
+        )
+      ).toStrictEqual([section]))
+
+    it('throws error when <Section> has unexpected component', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Section>
+              <Divider />
+            </Section>
+          </Blocks>
+        )
+      ).toThrow(/unexpected/))
+  })
+
+  describe('<Section> with accessory', () => {
+    const section: SectionBlock = {
+      type: 'section',
+      block_id: 'with_image',
+      text: { type: 'mrkdwn', text: 'Image example', verbatim: true },
+      accessory: {
+        type: 'image',
+        image_url: 'https://example.com/image.jpg',
+        alt_text: 'Example image',
+      },
+    }
+
+    it('outputs section block with image accessories', () => {
+      for (const accessory of [
+        <Image src="https://example.com/image.jpg" alt="Example image" />,
+        <img src="https://example.com/image.jpg" alt="Example image" />,
+      ]) {
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Section blockId="with_image">
+                Image example
+                {accessory}
+              </Section>
+            </Blocks>
+          )
+        ).toStrictEqual([section])
+      }
+    })
+
+    it('outputs section block with block element accessories', () => {
+      for (const accessory of [
+        <Button>Button</Button>,
+        <button>button</button>,
+        <Select>
+          <Option value="select">Static select</Option>
+        </Select>,
+        <Select multiple>
+          <Option value="select">Multiple select</Option>
+        </Select>,
+        <select>
+          <option value="select">intrinsic &lt;select&gt;</option>
+        </select>,
+        <select multiple>
+          <optgroup label="group">
+            <option value="select">intrinsic multi-select</option>
+          </optgroup>
+        </select>,
+        <ExternalSelect />,
+        <ExternalSelect multiple />,
+        <UsersSelect />,
+        <UsersSelect multiple />,
+        <ConversationsSelect />,
+        <ConversationsSelect multiple />,
+        <ChannelsSelect />,
+        <ChannelsSelect multiple />,
+        <Overflow>
+          <OverflowItem>Overflow</OverflowItem>
+          <OverflowItem>item</OverflowItem>
+        </Overflow>,
+        <DatePicker />,
+      ]) {
+        expect(
+          JSXSlack(
+            <Blocks>
+              <Section blockId="with_image">test {accessory}</Section>
+            </Blocks>
+          )
+        ).toStrictEqual([
+          expect.objectContaining({
+            accessory: expect.objectContaining({ type: expect.any(String) }),
+          }),
+        ])
+      }
+    })
+
+    it('outputs section block with multi-select menus', () => {
+      // Static multiple select
+      const [s] = JSXSlack(
+        <Blocks>
+          <Section>
+            Select
+            <Select multiple maxSelectedItems={2} value={['a', 'c']}>
+              <Option value="a">a</Option>
+              <Option value="b">b</Option>
+              <Option value="c">c</Option>
+            </Select>
+          </Section>
+        </Blocks>
+      )
+
+      expect(s.accessory.type).toBe('multi_static_select')
+      expect(s.accessory.max_selected_items).toBe(2)
+      expect(s.accessory.initial_options).toHaveLength(2)
+
+      // Multiple select for external sources
+      for (const accessory of [
+        <ExternalSelect
+          multiple
+          maxSelectedItems={2}
+          initialOption={<Option value="a">a</Option>}
+        />,
+        <UsersSelect multiple maxSelectedItems={2} initialUser="U00000000" />,
+        <ConversationsSelect
+          multiple
+          maxSelectedItems={2}
+          initialConversation={['C00000000']}
+        />,
+        <ChannelsSelect
+          multiple
+          maxSelectedItems={2}
+          initialChannel="D00000000"
+        />,
+      ]) {
+        const [ms] = JSXSlack(
+          <Blocks>
+            <Section>Select {accessory}</Section>
+          </Blocks>
+        )
+
+        expect(ms.accessory.type.startsWith('multi_')).toBe(true)
+        expect(ms.accessory.max_selected_items).toBe(2)
+
+        const initialKey: any = Object.keys(ms.accessory).find(k =>
+          k.startsWith('initial_')
+        )
+        expect(ms.accessory[initialKey]).toHaveLength(1)
+      }
+    })
+  })
+
+  describe('<Section> with fields', () => {
+    const section: SectionBlock = {
+      type: 'section',
+      block_id: 'fields',
+      fields: [
+        {
+          type: 'mrkdwn',
+          text: '*Field A*\n123',
+          verbatim: true,
+        },
+        {
+          type: 'mrkdwn',
+          text: '*Field B*\n456',
+          verbatim: true,
+        },
+      ],
+    }
+
+    it('outputs section block with fields option', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Section blockId="fields">
+              <Field>
+                <b>Field A</b>
+                <br />
+                {123}
+              </Field>
+              <Field>
+                <b>Field B</b>
+                <br />
+                {456}
+              </Field>
+            </Section>
+          </Blocks>
+        )
+      ).toStrictEqual([section]))
+  })
+
+  describe('<Divider>', () => {
+    const divider: DividerBlock = {
+      type: 'divider',
+      block_id: 'divider',
+    }
+
+    it('outputs divider block', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Divider blockId="divider" />
+          </Blocks>
+        )
+      ).toStrictEqual([divider]))
+
+    it('allows using HTML-compatible <hr> element', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <hr id="divider" />
+          </Blocks>
+        )
+      ).toStrictEqual([divider]))
+  })
+
+  describe('<Image>', () => {
+    const image: ImageBlock = {
+      type: 'image',
+      image_url: 'https://example.com/test.jpg',
+      alt_text: 'Test image',
+      title: {
+        type: 'plain_text',
+        text: 'This is a test image!',
+        emoji: true,
+      },
+      block_id: 'image',
+    }
+
+    it('outputs image block', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Image
+              src="https://example.com/test.jpg"
+              alt="Test image"
+              title="This is a test image!"
+              blockId="image"
+            />
+          </Blocks>
+        )
+      ).toStrictEqual([image]))
+
+    it('allows using HTML-compatible <img> element', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <img
+              src="https://example.com/test.jpg"
+              alt="Test image"
+              title="This is a test image!"
+              id="image"
+            />
+          </Blocks>
+        )
+      ).toStrictEqual([image]))
+  })
+
+  describe('<Actions>', () => {
+    it('throws error when the number of elements is 26', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Actions>
+              {[...Array(26)].map(() => (
+                <Button>btn</Button>
+              ))}
+            </Actions>
+          </Blocks>
+        )
+      ).toThrow())
+  })
+
+  describe('<Context>', () => {
+    it('outputs context block', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Context blockId="context">
+              Hello! <b>World!</b>
+              <img src="https://example.com/test.jpg" alt="image" />
+              Image + Text
+              <Image
+                src="https://example.com/test2.jpg"
+                alt="image component"
+              />
+            </Context>
+          </Blocks>
+        )
+      ).toStrictEqual([
+        {
+          type: 'context',
+          block_id: 'context',
+          elements: [
+            {
+              type: 'mrkdwn',
+              text: 'Hello! *World!*',
+              verbatim: true,
+            },
+            {
+              type: 'image',
+              image_url: 'https://example.com/test.jpg',
+              alt_text: 'image',
+            },
+            {
+              type: 'mrkdwn',
+              text: 'Image + Text',
+              verbatim: true,
+            },
+            {
+              type: 'image',
+              image_url: 'https://example.com/test2.jpg',
+              alt_text: 'image component',
+            },
+          ],
+        },
+      ]))
+
+    it('converts <span> elements into mrkdwn elements', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <Context>
+              <span>
+                <b>A</b>
+              </span>
+              B<br />C<span>D</span>
+              <span>
+                E <i>F</i> G
+              </span>
+              H
+            </Context>
+          </Blocks>
+        )
+      ).toStrictEqual([
+        {
+          type: 'context',
+          elements: [
+            { type: 'mrkdwn', text: '*A*', verbatim: true },
+            { type: 'mrkdwn', text: 'B\nC', verbatim: true },
+            { type: 'mrkdwn', text: 'D', verbatim: true },
+            { type: 'mrkdwn', text: 'E _F_ G', verbatim: true },
+            { type: 'mrkdwn', text: 'H', verbatim: true },
+          ],
+        },
+      ]))
+
+    it('throws error when the number of elements is 11', () =>
+      expect(() =>
+        JSXSlack(
+          <Blocks>
+            <Context>
+              <img src="foo" alt="1" />
+              2
+              <img src="foo" alt="3" />
+              4
+              <img src="foo" alt="5" />
+              6
+              <img src="foo" alt="7" />
+              7
+              <img src="foo" alt="9" />
+              10
+              <img src="foo" alt="11" />
+            </Context>
+          </Blocks>
+        )
+      ).toThrow())
+  })
+
+  describe('<File>', () => {
+    const file: FileBlock = {
+      block_id: 'file',
+      external_id: 'ABCD1',
+      source: 'remote',
+      type: 'file',
+    }
+
+    it('outputs file block', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <File blockId="file" externalId="ABCD1" />
+          </Blocks>
+        )
+      ).toStrictEqual([file]))
+
+    it('allows overriding source prop for future use', () =>
+      expect(
+        JSXSlack(
+          <Blocks>
+            <File id="file" externalId="ABCD1" source="local" />
+          </Blocks>
+        )
+      ).toStrictEqual([{ ...file, source: 'local' }]))
+  })
+
+  describe('<Input> (layout block)', () => {
+    it('outputs input block with wrapped element', () => {
+      const select = (
+        <Select>
+          <Option value="test">test</Option>
+        </Select>
+      )
+
+      const expected: InputBlock = {
+        type: 'input',
+        block_id: 'input-id',
+        label: { type: 'plain_text', text: 'Select', emoji: true },
+        hint: { type: 'plain_text', text: 'foobar', emoji: true },
+        optional: true,
+        element: JSXSlack(select),
+      }
+
+      const { blocks } = JSXSlack(
+        <Modal title="test">
+          <Input blockId="input-id" label="Select" hint="foobar">
+            {select}
+          </Input>
+        </Modal>
+      )
+
+      expect(blocks).toStrictEqual([expected])
+
+      // HTML-compatible aliases
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <Input
+              id="input-id"
+              label="Select"
+              title="foobar"
+              children={select}
+            />
+          </Modal>
+        ).blocks
+      ).toStrictEqual(blocks)
+
+      // Intrinsic HTML elements
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <input
+              id="input-id"
+              label="Select"
+              title="foobar"
+              children={select}
+            />
+          </Modal>
+        ).blocks
+      ).toStrictEqual(blocks)
+
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <input id="input-id" label="Select" title="foobar">
+              {select}
+            </input>
+          </Modal>
+        ).blocks
+      ).toStrictEqual(blocks)
+
+      expect(
+        JSXSlack(
+          <Modal title="test">
+            <select id="input-id" label="Select" title="foobar">
+              <option value="test">test</option>
+            </select>
+          </Modal>
+        ).blocks
+      ).toStrictEqual(blocks)
+    })
+
+    it('throws error when wrapped invalid element', () => {
+      expect(() =>
+        JSXSlack(
+          <Modal title="test">
+            <Input label="invalid">
+              <Overflow actionId="overflow">
+                <OverflowItem value="a">A</OverflowItem>
+                <OverflowItem value="b">B</OverflowItem>
+              </Overflow>
+            </Input>
+          </Modal>
+        )
+      ).toThrow(/invalid/)
+    })
+  })
+})
diff --git a/test/index.tsx b/test/index.tsx
index 5a9a956..3f58f99 100644
--- a/test/index.tsx
+++ b/test/index.tsx
@@ -1,1682 +1,24 @@
 /** @jsx JSXSlack.h */
-import {
-  ActionsBlock,
-  DividerBlock,
-  FileBlock,
-  ImageBlock,
-  InputBlock,
-  SectionBlock,
-  StaticSelect,
-  Option as SlackOption,
-  Overflow as SlackOverflow,
-  View,
-} from '@slack/types'
-import JSXSlack, {
-  Actions,
-  Blocks,
-  Button,
-  ChannelsSelect,
-  Confirm,
-  Context,
-  ConversationsSelect,
-  DatePicker,
-  Divider,
-  Escape,
-  ExternalSelect,
-  Field,
-  File,
-  Fragment,
-  Image,
-  Input,
-  Modal,
-  Optgroup,
-  Option,
-  Overflow,
-  OverflowItem,
-  Section,
-  Select,
-  SelectFragment,
-  Textarea,
-  UsersSelect,
-} from '../src/index'
+import JSXSlack, { Option, SelectFragment } from '../src/index'
 import { jsxOnParsed } from '../src/jsx'
 
 beforeEach(() => JSXSlack.exactMode(false))
 
-describe('jsx-slack', () => {
-  describe('#JSXSlack', () => {
-    it('executes specified func after parsing if passed internal node has jsxOnParsed symbol prop', () => {
-      const onParsed = jest.fn()
-      const node = (
-        <SelectFragment>
-          <Option value="a">A</Option>
-        </SelectFragment>
-      )
+describe('#JSXSlack', () => {
+  it('executes specified func after parsing if passed internal node has jsxOnParsed symbol prop', () => {
+    const onParsed = jest.fn()
+    const node = (
+      <SelectFragment>
+        <Option value="a">A</Option>
+      </SelectFragment>
+    )
 
-      node.props[jsxOnParsed] = onParsed
-      JSXSlack(node)
+    node.props[jsxOnParsed] = onParsed
+    JSXSlack(node)
 
-      expect(onParsed).toBeCalledTimes(1)
-    })
-
-    it('throws error by passed invalid node', () =>
-      expect(() => JSXSlack({ props: {}, type: -1 } as any)).toThrow())
-  })
-
-  describe('Container components', () => {
-    describe('<Blocks>', () => {
-      it('throws error when <Blocks> has unexpected element', () => {
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <b>unexpected</b>
-            </Blocks>
-          )
-        ).toThrow()
-
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Escape>unexpected</Escape>
-            </Blocks>
-          )
-        ).toThrow()
-
-        // <Input> block cannot use in message
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Input label="Select">
-                <Select>
-                  <Option value="test">test</Option>
-                </Select>
-              </Input>
-            </Blocks>
-          )
-        ).toThrow()
-      })
-    })
-
-    describe('<Modal>', () => {
-      it('generates view payload JSON', () => {
-        const simpleView: View = {
-          type: 'modal',
-          title: { type: 'plain_text', text: 'test', emoji: true },
-          blocks: [{ type: 'section', text: expect.any(Object) }],
-        }
-
-        expect(
-          JSXSlack(
-            <Modal title="test">
-              <Section>Hello!</Section>
-            </Modal>
-          )
-        ).toStrictEqual(simpleView)
-
-        // Optional attributes
-        const viewWithOptions: View & Record<string, any> = {
-          type: 'modal',
-          title: expect.any(Object),
-          blocks: expect.any(Array),
-          submit: { type: 'plain_text', text: 'Submit', emoji: true },
-          close: { type: 'plain_text', text: 'Close', emoji: true },
-          private_metadata: 'private_metadata',
-          clear_on_close: true,
-          notify_on_close: false,
-
-          // Fields for API
-          callback_id: 'callback_id',
-          external_id: 'external_id',
-        }
-
-        expect(
-          JSXSlack(
-            <Modal
-              callbackId="callback_id"
-              clearOnClose
-              close="Close"
-              externalId="external_id"
-              notifyOnClose={false}
-              privateMetadata="private_metadata"
-              submit="Submit"
-              title="test"
-            >
-              <Section>Hello!</Section>
-            </Modal>
-          )
-        ).toStrictEqual(viewWithOptions)
-      })
-    })
-  })
-
-  describe('Layout blocks', () => {
-    describe('<Section>', () => {
-      const section: SectionBlock = {
-        type: 'section',
-        block_id: 'hello',
-        text: { type: 'mrkdwn', text: 'Hello!', verbatim: true },
-      }
-
-      it('outputs section block', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Section blockId="hello">Hello!</Section>
-            </Blocks>
-          )
-        ).toStrictEqual([section]))
-
-      it('allows using HTML-compatible <section> element', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <section id="hello">Hello!</section>
-            </Blocks>
-          )
-        ).toStrictEqual([section]))
-
-      it('throws error when <Section> has unexpected component', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Section>
-                <Divider />
-              </Section>
-            </Blocks>
-          )
-        ).toThrow(/unexpected/))
-    })
-
-    describe('<Section> with accessory', () => {
-      const section: SectionBlock = {
-        type: 'section',
-        block_id: 'with_image',
-        text: { type: 'mrkdwn', text: 'Image example', verbatim: true },
-        accessory: {
-          type: 'image',
-          image_url: 'https://example.com/image.jpg',
-          alt_text: 'Example image',
-        },
-      }
-
-      it('outputs section block with image accessories', () => {
-        for (const accessory of [
-          <Image src="https://example.com/image.jpg" alt="Example image" />,
-          <img src="https://example.com/image.jpg" alt="Example image" />,
-        ]) {
-          expect(
-            JSXSlack(
-              <Blocks>
-                <Section blockId="with_image">
-                  Image example
-                  {accessory}
-                </Section>
-              </Blocks>
-            )
-          ).toStrictEqual([section])
-        }
-      })
-
-      it('outputs section block with action accessories', () => {
-        for (const accessory of [
-          <Button>Button</Button>,
-          <button>button</button>,
-          <Select>
-            <Option value="select">Static select</Option>
-          </Select>,
-          <Select multiple>
-            <Option value="select">Multiple select</Option>
-          </Select>,
-          <select>
-            <option value="select">intrinsic &lt;select&gt;</option>
-          </select>,
-          <select multiple>
-            <optgroup label="group">
-              <option value="select">intrinsic multi-select</option>
-            </optgroup>
-          </select>,
-          <ExternalSelect />,
-          <ExternalSelect multiple />,
-          <UsersSelect />,
-          <UsersSelect multiple />,
-          <ConversationsSelect />,
-          <ConversationsSelect multiple />,
-          <ChannelsSelect />,
-          <ChannelsSelect multiple />,
-          <Overflow>
-            <OverflowItem>Overflow</OverflowItem>
-            <OverflowItem>item</OverflowItem>
-          </Overflow>,
-          <DatePicker />,
-        ]) {
-          expect(
-            JSXSlack(
-              <Blocks>
-                <Section blockId="with_image">test {accessory}</Section>
-              </Blocks>
-            )
-          ).toStrictEqual([
-            expect.objectContaining({
-              accessory: expect.objectContaining({ type: expect.any(String) }),
-            }),
-          ])
-        }
-      })
-
-      it('outputs section block with multi-select menus', () => {
-        // Static multiple select
-        const [s] = JSXSlack(
-          <Blocks>
-            <Section>
-              Select
-              <Select multiple maxSelectedItems={2} value={['a', 'c']}>
-                <Option value="a">a</Option>
-                <Option value="b">b</Option>
-                <Option value="c">c</Option>
-              </Select>
-            </Section>
-          </Blocks>
-        )
-
-        expect(s.accessory.type).toBe('multi_static_select')
-        expect(s.accessory.max_selected_items).toBe(2)
-        expect(s.accessory.initial_options).toHaveLength(2)
-
-        // Multiple select for external sources
-        for (const accessory of [
-          <ExternalSelect
-            multiple
-            maxSelectedItems={2}
-            initialOption={<Option value="a">a</Option>}
-          />,
-          <UsersSelect multiple maxSelectedItems={2} initialUser="U00000000" />,
-          <ConversationsSelect
-            multiple
-            maxSelectedItems={2}
-            initialConversation={['C00000000']}
-          />,
-          <ChannelsSelect
-            multiple
-            maxSelectedItems={2}
-            initialChannel="D00000000"
-          />,
-        ]) {
-          const [ms] = JSXSlack(
-            <Blocks>
-              <Section>Select {accessory}</Section>
-            </Blocks>
-          )
-
-          expect(ms.accessory.type.startsWith('multi_')).toBe(true)
-          expect(ms.accessory.max_selected_items).toBe(2)
-
-          const initialKey: any = Object.keys(ms.accessory).find(k =>
-            k.startsWith('initial_')
-          )
-          expect(ms.accessory[initialKey]).toHaveLength(1)
-        }
-      })
-    })
-
-    describe('<Section> with fields', () => {
-      const section: SectionBlock = {
-        type: 'section',
-        block_id: 'fields',
-        fields: [
-          {
-            type: 'mrkdwn',
-            text: '*Field A*\n123',
-            verbatim: true,
-          },
-          {
-            type: 'mrkdwn',
-            text: '*Field B*\n456',
-            verbatim: true,
-          },
-        ],
-      }
-
-      it('outputs section block with fields option', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Section blockId="fields">
-                <Field>
-                  <b>Field A</b>
-                  <br />
-                  {123}
-                </Field>
-                <Field>
-                  <b>Field B</b>
-                  <br />
-                  {456}
-                </Field>
-              </Section>
-            </Blocks>
-          )
-        ).toStrictEqual([section]))
-    })
-
-    describe('<Divider>', () => {
-      const divider: DividerBlock = {
-        type: 'divider',
-        block_id: 'divider',
-      }
-
-      it('outputs divider block', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Divider blockId="divider" />
-            </Blocks>
-          )
-        ).toStrictEqual([divider]))
-
-      it('allows using HTML-compatible <hr> element', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <hr id="divider" />
-            </Blocks>
-          )
-        ).toStrictEqual([divider]))
-    })
-
-    describe('<Image>', () => {
-      const image: ImageBlock = {
-        type: 'image',
-        image_url: 'https://example.com/test.jpg',
-        alt_text: 'Test image',
-        title: {
-          type: 'plain_text',
-          text: 'This is a test image!',
-          emoji: true,
-        },
-        block_id: 'image',
-      }
-
-      it('outputs image block', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Image
-                src="https://example.com/test.jpg"
-                alt="Test image"
-                title="This is a test image!"
-                blockId="image"
-              />
-            </Blocks>
-          )
-        ).toStrictEqual([image]))
-
-      it('allows using HTML-compatible <img> element', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <img
-                src="https://example.com/test.jpg"
-                alt="Test image"
-                title="This is a test image!"
-                id="image"
-              />
-            </Blocks>
-          )
-        ).toStrictEqual([image]))
-    })
-
-    describe('<Actions>', () => {
-      const action = (...elements: ActionsBlock['elements']): ActionsBlock => ({
-        block_id: 'actions',
-        elements,
-        type: 'actions',
-      })
-
-      it('outputs actions block with <Button> for action', () => {
-        const buttonAction = action({
-          type: 'button',
-          action_id: 'action',
-          text: { type: 'plain_text', text: 'Hello!', emoji: true },
-          value: 'value',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Button actionId="action" value="value">
-                  Hello!
-                </Button>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([buttonAction])
-      })
-
-      it('allows using HTML-compatible <button> element', () => {
-        const buttonAction = action({
-          type: 'button',
-          action_id: 'action',
-          text: { type: 'plain_text', text: 'Hello!', emoji: true },
-          value: 'value',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <button name="action" value="value">
-                  Hello!
-                </button>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([buttonAction])
-      })
-
-      it('outputs actions block with <Button> for link', () => {
-        const buttonAction = action({
-          type: 'button',
-          url: 'https://example.com/',
-          text: { type: 'plain_text', text: 'Link Button', emoji: true },
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Button url="https://example.com/">Link Button</Button>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([buttonAction])
-      })
-
-      it('outputs actions block with styled <Button>', () => {
-        const buttonAction = action(
-          {
-            type: 'button',
-            text: { type: 'plain_text', text: 'Default', emoji: true },
-          },
-          {
-            type: 'button',
-            text: { type: 'plain_text', text: 'Primary', emoji: true },
-            style: 'primary',
-          },
-          {
-            type: 'button',
-            text: { type: 'plain_text', text: 'Danger', emoji: true },
-            style: 'danger',
-          }
-        )
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Button>Default</Button>
-                <Button style="primary">Primary</Button>
-                <Button style="danger">Danger</Button>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([buttonAction])
-      })
-
-      it('outputs actions block with <Select> for static items', () => {
-        const selectAction = action({
-          type: 'static_select',
-          action_id: 'select',
-          placeholder: { type: 'plain_text', text: 'Select box', emoji: true },
-          options: [
-            {
-              text: { type: 'plain_text', text: '1st', emoji: true },
-              value: 'first',
-            },
-            {
-              text: { type: 'plain_text', text: '2nd', emoji: true },
-              value: 'second',
-            },
-            {
-              text: { type: 'plain_text', text: '3rd', emoji: true },
-              value: 'third',
-            },
-          ],
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Select actionId="select" placeholder="Select box">
-                  <Option value="first">1st</Option>
-                  <Option value="second">2nd</Option>
-                  <Option value="third">3rd</Option>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <Select> and <Optgroup>', () => {
-        const selectAction = action({
-          type: 'static_select',
-          action_id: 'select',
-          placeholder: { type: 'plain_text', text: 'Group', emoji: true },
-          option_groups: [
-            {
-              label: { type: 'plain_text', text: 'A', emoji: true },
-              options: [
-                {
-                  text: { type: 'plain_text', text: '1st', emoji: true },
-                  value: 'first',
-                },
-                {
-                  text: { type: 'plain_text', text: '2nd', emoji: true },
-                  value: 'second',
-                },
-              ],
-            },
-            {
-              label: { type: 'plain_text', text: 'B', emoji: true },
-              options: [
-                {
-                  text: { type: 'plain_text', text: '3rd', emoji: true },
-                  value: 'third',
-                },
-                {
-                  text: { type: 'plain_text', text: '4th', emoji: true },
-                  value: 'fourth',
-                },
-              ],
-            },
-          ],
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Select actionId="select" placeholder="Group">
-                  <Optgroup label="A">
-                    <Option value="first">1st</Option>
-                    <Option value="second">2nd</Option>
-                  </Optgroup>
-                  <Optgroup label="B">
-                    <Option value="third">3rd</Option>
-                    <Option value="fourth">4th</Option>
-                  </Optgroup>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <Select> and initial option', () => {
-        const selectAction = action({
-          type: 'static_select',
-          action_id: 'select',
-          options: [
-            {
-              text: { type: 'plain_text', text: '1st', emoji: true },
-              value: 'first',
-            },
-            {
-              text: { type: 'plain_text', text: '2nd', emoji: true },
-              value: 'second',
-            },
-            {
-              text: { type: 'plain_text', text: '3rd', emoji: true },
-              value: 'third',
-            },
-          ],
-          initial_option: {
-            text: { type: 'plain_text', text: '3rd', emoji: true },
-            value: 'third',
-          },
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Select actionId="select" value="third">
-                  <Option value="first">1st</Option>
-                  <Option value="second">2nd</Option>
-                  <Option value="third">3rd</Option>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('allows using HTML-compatible <select>, <option> and <optgroup> elements', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <select name="test">
-                  <optgroup label="foo">
-                    <option value="bar">bar</option>
-                  </optgroup>
-                </select>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual(
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Select actionId="test">
-                  <Optgroup label="foo">
-                    <Option value="bar">bar</Option>
-                  </Optgroup>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ))
-
-      it('throws error when <Select> has not contained <Option>', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Select>{}</Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow(/must include/))
-
-      it('throws error when <Select> has mixed children', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Select>
-                  <Option value="first">1st</Option>
-                  <Optgroup label="A">
-                    <Option value="second">2nd</Option>
-                    <Option value="third">3rd</Option>
-                  </Optgroup>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow(/only include either of/))
-
-      it('outputs actions block with <ExternalSelect> for external items', () => {
-        const initialOption: SlackOption = {
-          text: { type: 'plain_text', text: 'value', emoji: false },
-          value: 'value',
-        }
-
-        const selectAction = action({
-          type: 'external_select',
-          action_id: 'external',
-          placeholder: { type: 'plain_text', text: 'External', emoji: true },
-          min_query_length: 4,
-          initial_option: initialOption,
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <ExternalSelect
-                  actionId="external"
-                  placeholder="External"
-                  minQueryLength={4}
-                  initialOption={initialOption}
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <ExternalSelect> and initial option defined by <Option>', () => {
-        const selectAction = action({
-          type: 'external_select',
-          initial_option: {
-            text: { type: 'plain_text', text: 'Option value', emoji: true },
-            value: 'option',
-          },
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <ExternalSelect
-                  initialOption={<Option value="option">Option value</Option>}
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <UsersSelect>', () => {
-        const selectAction = action({
-          type: 'users_select',
-          action_id: 'users',
-          placeholder: { type: 'plain_text', text: 'Select user', emoji: true },
-          initial_user: 'U01234567',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <UsersSelect
-                  actionId="users"
-                  placeholder="Select user"
-                  initialUser="U01234567"
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <ConversationsSelect>', () => {
-        const selectAction = action({
-          type: 'conversations_select',
-          action_id: 'conversations',
-          placeholder: {
-            type: 'plain_text',
-            text: 'Select conversation',
-            emoji: true,
-          },
-          initial_conversation: 'C89ABCDEF',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <ConversationsSelect
-                  actionId="conversations"
-                  placeholder="Select conversation"
-                  initialConversation="C89ABCDEF"
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <ChannelsSelect>', () => {
-        const selectAction = action({
-          type: 'channels_select',
-          action_id: 'channels',
-          placeholder: {
-            type: 'plain_text',
-            text: 'Select channel',
-            emoji: true,
-          },
-          initial_channel: 'C98765432',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <ChannelsSelect
-                  actionId="channels"
-                  placeholder="Select channel"
-                  initialChannel="C98765432"
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([selectAction])
-      })
-
-      it('outputs actions block with <Overflow>', () => {
-        const baseOverflow: SlackOverflow = {
-          type: 'overflow',
-          action_id: 'overflow_menu',
-          options: [
-            {
-              text: { type: 'plain_text', text: 'Menu A', emoji: true },
-              value: 'menu_a',
-            },
-            {
-              text: { type: 'plain_text', text: 'Menu B', emoji: true },
-              value: 'menu_b',
-            },
-            {
-              text: { type: 'plain_text', text: 'Menu C', emoji: true },
-              value: 'menu_c',
-            },
-            {
-              text: { type: 'plain_text', text: 'Link', emoji: true },
-              url: 'https://example.com/',
-            },
-          ],
-        }
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Overflow actionId="overflow_menu">
-                  <OverflowItem value="menu_a">Menu A</OverflowItem>
-                  <OverflowItem value="menu_b">Menu B</OverflowItem>
-                  <OverflowItem value="menu_c">Menu C</OverflowItem>
-                  <OverflowItem url="https://example.com/">Link</OverflowItem>
-                </Overflow>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([action(baseOverflow)])
-
-        // confirm prop and HTML-compatible props
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions id="actions">
-                <Overflow
-                  name="overflow_menu"
-                  confirm={
-                    <Confirm title="a" confirm="b" deny="c">
-                      foobar
-                    </Confirm>
-                  }
-                >
-                  <OverflowItem value="menu_a">Menu A</OverflowItem>
-                  <OverflowItem value="menu_b">Menu B</OverflowItem>
-                  <OverflowItem value="menu_c">Menu C</OverflowItem>
-                  <OverflowItem url="https://example.com/">Link</OverflowItem>
-                </Overflow>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([
-          action({
-            ...baseOverflow,
-            confirm: {
-              title: { type: 'plain_text', text: 'a', emoji: true },
-              confirm: { type: 'plain_text', text: 'b', emoji: true },
-              deny: { type: 'plain_text', text: 'c', emoji: true },
-              text: { type: 'mrkdwn', text: 'foobar', verbatim: true },
-            },
-          }),
-        ])
-      })
-
-      it('throws error when <Overflow> has only one <OverflowItem>', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Overflow>
-                  <OverflowItem value="a">A</OverflowItem>
-                </Overflow>
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow())
-
-      it('throws error when <Overflow> has unexpected children', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Overflow>
-                  <Button>btn</Button>
-                  <Button>btn</Button>
-                </Overflow>
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow())
-
-      it('outputs actions block with <DatePicker>', () => {
-        const datePickerAction = action({
-          type: 'datepicker',
-          action_id: 'date_picker',
-          placeholder: { type: 'plain_text', text: 'Select date', emoji: true },
-          initial_date: '2019-01-23',
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <DatePicker
-                  actionId="date_picker"
-                  placeholder="Select date"
-                  initialDate={new Date(1548214496000)} // 2019-01-23 12:34:56
-                />
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([datePickerAction])
-      })
-
-      it('outputs actions block with action included <Confirm> object', () => {
-        const buttonAction = action({
-          type: 'button',
-          text: { type: 'plain_text', text: 'Share', emoji: true },
-          confirm: {
-            confirm: { type: 'plain_text', text: 'Yes, please', emoji: true },
-            deny: { type: 'plain_text', text: 'Cancel', emoji: true },
-            title: { type: 'plain_text', text: 'Share to SNS', emoji: true },
-            text: {
-              type: 'mrkdwn',
-              text: '*Are you sure?* Message will be share.',
-              verbatim: true,
-            },
-          },
-        })
-
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Actions blockId="actions">
-                <Button
-                  confirm={
-                    <Confirm
-                      title="Share to SNS"
-                      confirm="Yes, please"
-                      deny="Cancel"
-                    >
-                      <b>Are you sure?</b> Message will be share.
-                    </Confirm>
-                  }
-                >
-                  Share
-                </Button>
-              </Actions>
-            </Blocks>
-          )
-        ).toStrictEqual([buttonAction])
-      })
-
-      it('throws error when passed selectable element with "multiple" prop', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                <Select multiple>
-                  <Option value="error">error</Option>
-                </Select>
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow())
-
-      it('throws error when the number of elements is 26', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Actions>
-                {[...Array(26)].map(() => (
-                  <Button>btn</Button>
-                ))}
-              </Actions>
-            </Blocks>
-          )
-        ).toThrow())
-    })
-
-    describe('<Context>', () => {
-      it('outputs context block', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Context blockId="context">
-                Hello! <b>World!</b>
-                <img src="https://example.com/test.jpg" alt="image" />
-                Image + Text
-                <Image
-                  src="https://example.com/test2.jpg"
-                  alt="image component"
-                />
-              </Context>
-            </Blocks>
-          )
-        ).toStrictEqual([
-          {
-            type: 'context',
-            block_id: 'context',
-            elements: [
-              {
-                type: 'mrkdwn',
-                text: 'Hello! *World!*',
-                verbatim: true,
-              },
-              {
-                type: 'image',
-                image_url: 'https://example.com/test.jpg',
-                alt_text: 'image',
-              },
-              {
-                type: 'mrkdwn',
-                text: 'Image + Text',
-                verbatim: true,
-              },
-              {
-                type: 'image',
-                image_url: 'https://example.com/test2.jpg',
-                alt_text: 'image component',
-              },
-            ],
-          },
-        ]))
-
-      it('converts <span> elements into mrkdwn elements', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <Context>
-                <span>
-                  <b>A</b>
-                </span>
-                B<br />C<span>D</span>
-                <span>
-                  E <i>F</i> G
-                </span>
-                H
-              </Context>
-            </Blocks>
-          )
-        ).toStrictEqual([
-          {
-            type: 'context',
-            elements: [
-              { type: 'mrkdwn', text: '*A*', verbatim: true },
-              { type: 'mrkdwn', text: 'B\nC', verbatim: true },
-              { type: 'mrkdwn', text: 'D', verbatim: true },
-              { type: 'mrkdwn', text: 'E _F_ G', verbatim: true },
-              { type: 'mrkdwn', text: 'H', verbatim: true },
-            ],
-          },
-        ]))
-
-      it('throws error when the number of elements is 11', () =>
-        expect(() =>
-          JSXSlack(
-            <Blocks>
-              <Context>
-                <img src="foo" alt="1" />
-                2
-                <img src="foo" alt="3" />
-                4
-                <img src="foo" alt="5" />
-                6
-                <img src="foo" alt="7" />
-                7
-                <img src="foo" alt="9" />
-                10
-                <img src="foo" alt="11" />
-              </Context>
-            </Blocks>
-          )
-        ).toThrow())
-    })
-
-    describe('<File>', () => {
-      const file: FileBlock = {
-        block_id: 'file',
-        external_id: 'ABCD1',
-        source: 'remote',
-        type: 'file',
-      }
-
-      it('outputs file block', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <File blockId="file" externalId="ABCD1" />
-            </Blocks>
-          )
-        ).toStrictEqual([file]))
-
-      it('allows overriding source prop for future use', () =>
-        expect(
-          JSXSlack(
-            <Blocks>
-              <File id="file" externalId="ABCD1" source="local" />
-            </Blocks>
-          )
-        ).toStrictEqual([{ ...file, source: 'local' }]))
-    })
-
-    describe('<Input>', () => {
-      describe('as layout block', () => {
-        it('outputs input block with wrapped element', () => {
-          const select = (
-            <Select>
-              <Option value="test">test</Option>
-            </Select>
-          )
-
-          const expected: InputBlock = {
-            type: 'input',
-            block_id: 'input-id',
-            label: { type: 'plain_text', text: 'Select', emoji: true },
-            hint: { type: 'plain_text', text: 'foobar', emoji: true },
-            optional: true,
-            element: JSXSlack(select),
-          }
-
-          const { blocks } = JSXSlack(
-            <Modal title="test">
-              <Input blockId="input-id" label="Select" hint="foobar">
-                {select}
-              </Input>
-            </Modal>
-          )
-
-          expect(blocks).toStrictEqual([expected])
-
-          // HTML-compatible aliases
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <Input
-                  id="input-id"
-                  label="Select"
-                  title="foobar"
-                  children={select}
-                />
-              </Modal>
-            ).blocks
-          ).toStrictEqual(blocks)
-
-          // Intrinsic HTML elements
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <input
-                  id="input-id"
-                  label="Select"
-                  title="foobar"
-                  children={select}
-                />
-              </Modal>
-            ).blocks
-          ).toStrictEqual(blocks)
-
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <input id="input-id" label="Select" title="foobar">
-                  {select}
-                </input>
-              </Modal>
-            ).blocks
-          ).toStrictEqual(blocks)
-
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <select id="input-id" label="Select" title="foobar">
-                  <option value="test">test</option>
-                </select>
-              </Modal>
-            ).blocks
-          ).toStrictEqual(blocks)
-        })
-
-        it('throws error when wrapped invalid element', () => {
-          expect(() =>
-            JSXSlack(
-              <Modal title="test">
-                <Input label="invalid">
-                  <Overflow actionId="overflow">
-                    <OverflowItem value="a">A</OverflowItem>
-                    <OverflowItem value="b">B</OverflowItem>
-                  </Overflow>
-                </Input>
-              </Modal>
-            )
-          ).toThrow(/invalid/)
-        })
-      })
-
-      describe('as block element for plain-text input', () => {
-        it('outputs input block with plain-text input element', () => {
-          const { blocks } = JSXSlack(
-            <Modal title="test">
-              <Input actionId="action" blockId="foo" hint="bar" label="Input" />
-            </Modal>
-          )
-
-          const expected: InputBlock = {
-            type: 'input',
-            block_id: 'foo',
-            label: { type: 'plain_text', text: 'Input', emoji: true },
-            hint: { type: 'plain_text', text: 'bar', emoji: true },
-            optional: true,
-            element: {
-              type: 'plain_text_input',
-              action_id: 'action',
-            },
-          }
-
-          expect(blocks).toStrictEqual([expected])
-
-          // HTML-compatible aliases
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <Input id="foo" label="Input" name="action" title="bar" />
-              </Modal>
-            ).blocks
-          ).toStrictEqual(blocks)
-
-          // Placeholder must disable emoji
-          const { blocks: blocksPlaceholder } = JSXSlack(
-            <Modal title="test">
-              <Input label="placeholder" placeholder="Hi 😃" />
-            </Modal>
-          )
-
-          expect(blocksPlaceholder[0].element.placeholder).toStrictEqual({
-            type: 'plain_text',
-            text: 'Hi 😃',
-            emoji: false,
-          })
-        })
-      })
-    })
-  })
-
-  describe('Block elements', () => {
-    describe('Input-compatible components', () => {
-      it('wraps element for select in <Input> block when passed label prop', () => {
-        for (const Selectable of [
-          props => (
-            <Select {...props}>
-              <Option value="test">test</Option>
-            </Select>
-          ),
-          ExternalSelect,
-          ChannelsSelect,
-          ConversationsSelect,
-          UsersSelect,
-          DatePicker,
-        ]) {
-          expect(
-            JSXSlack(
-              <Modal title="test">
-                <Selectable id="test" label="Test" name="name" title="foobar" />
-              </Modal>
-            )
-          ).toStrictEqual(
-            JSXSlack(
-              <Modal title="test">
-                <Input blockId="test" label="Test" hint="foobar">
-                  <Selectable actionId="name" />
-                </Input>
-              </Modal>
-            )
-          )
-        }
-      })
-    })
-
-    describe('<Textarea>', () => {
-      const expected: InputBlock = {
-        type: 'input',
-        label: { type: 'plain_text', text: 'textarea', emoji: true },
-        optional: true,
-        element: {
-          type: 'plain_text_input',
-          action_id: 'foobar',
-          multiline: true,
-        },
-      }
-
-      it('outputs input block with plain-text input element that is enabled multiline prop', () => {
-        const { blocks } = JSXSlack(
-          <Modal title="test">
-            <Textarea label="textarea" name="foobar" />
-          </Modal>
-        )
-
-        expect(blocks).toStrictEqual([expected])
-      })
-
-      it('allows using HTML-compatible <textarea> element', () => {
-        const { blocks } = JSXSlack(
-          <Modal title="test">
-            <textarea label="textarea" name="foobar" />
-          </Modal>
-        )
-
-        expect(blocks).toStrictEqual([expected])
-      })
-    })
-  })
-
-  describe('<Escape> component', () => {
-    it('replaces special character in wrapped by <Escape> component', () =>
-      expect(
-        JSXSlack(
-          <Blocks>
-            <Section>&gt; *bold* _italic_ ~strikethrough~ `code`</Section>
-            <Section>
-              <Escape>&gt; *bold* _italic_ ~strikethrough~ `code`</Escape>
-            </Section>
-          </Blocks>
-        )
-      ).toStrictEqual([
-        expect.objectContaining({
-          text: expect.objectContaining({
-            text: '&gt; *bold* _italic_ ~strikethrough~ `code`',
-          }),
-        }),
-        expect.objectContaining({
-          text: expect.objectContaining({
-            text:
-              '\u00ad&gt; \u2217bold\u2217 \u02cditalic\u02cd \u223cstrikethrough\u223c \u02cbcode\u02cb',
-          }),
-        }),
-      ]))
-  })
-
-  describe('<Fragment> component', () => {
-    it('allows grouping multiple components for custom block', () => {
-      const CustomBlock: JSXSlack.FC<{ children: JSXSlack.Children<{}> }> = ({
-        children,
-      }) => (
-        <Fragment>
-          <Divider />
-          <Section>{children}</Section>
-          <Divider />
-        </Fragment>
-      )
-
-      expect(
-        JSXSlack(
-          <Blocks>
-            <CustomBlock>Hello!</CustomBlock>
-          </Blocks>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <Blocks>
-            <Divider />
-            <Section>Hello!</Section>
-            <Divider />
-          </Blocks>
-        )
-      )
-    })
-
-    it('allows grouping select options', () => {
-      const CustomOptions: JSXSlack.FC = () => (
-        <Fragment>
-          <Option value="a">A</Option>
-          <Option value="b">B</Option>
-        </Fragment>
-      )
-
-      // for <Select>
-      expect(
-        JSXSlack(
-          <Blocks>
-            <Actions>
-              <Select>
-                <CustomOptions />
-              </Select>
-            </Actions>
-          </Blocks>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <Blocks>
-            <Actions>
-              <Select>
-                <Option value="a">A</Option>
-                <Option value="b">B</Option>
-              </Select>
-            </Actions>
-          </Blocks>
-        )
-      )
-
-      // for <SelectFragment>
-      expect(
-        JSXSlack(
-          <SelectFragment>
-            <CustomOptions />
-          </SelectFragment>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <SelectFragment>
-            <Option value="a">A</Option>
-            <Option value="b">B</Option>
-          </SelectFragment>
-        )
-      )
-    })
-
-    it('allows grouping overflow items', () => {
-      const CustomOverflowItems: JSXSlack.FC = () => (
-        <Fragment>
-          <OverflowItem value="a">A</OverflowItem>
-          <OverflowItem value="b">B</OverflowItem>
-        </Fragment>
-      )
-
-      expect(
-        JSXSlack(
-          <Blocks>
-            <Actions>
-              <Overflow>
-                <CustomOverflowItems />
-              </Overflow>
-            </Actions>
-          </Blocks>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <Blocks>
-            <Actions>
-              <Overflow>
-                <OverflowItem value="a">A</OverflowItem>
-                <OverflowItem value="b">B</OverflowItem>
-              </Overflow>
-            </Actions>
-          </Blocks>
-        )
-      )
-    })
-
-    it('allows grouping multiple HTML elements', () => {
-      const CustomList: JSXSlack.FC = () => (
-        <Fragment>
-          <li>A</li>
-          <li>B</li>
-        </Fragment>
-      )
-
-      expect(
-        JSXSlack(
-          <Blocks>
-            <Section>
-              <ul>
-                <CustomList />
-              </ul>
-            </Section>
-          </Blocks>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <Blocks>
-            <Section>
-              <ul>
-                <li>A</li>
-                <li>B</li>
-              </ul>
-            </Section>
-          </Blocks>
-        )
-      )
-    })
-
-    it('allows nested fragments', () => {
-      expect(
-        JSXSlack(
-          <Blocks>
-            <Fragment>
-              <Fragment>
-                <Section>Nested</Section>
-                <Section>fragments</Section>
-              </Fragment>
-              <Fragment>
-                <Section>with</Section>
-                <Section>multiple</Section>
-              </Fragment>
-            </Fragment>
-            <Fragment>
-              <Fragment>
-                <Section>components</Section>
-                <Section>are</Section>
-              </Fragment>
-              <Fragment>
-                <Section>supported</Section>
-                <Section>well</Section>
-              </Fragment>
-            </Fragment>
-          </Blocks>
-        )
-      ).toStrictEqual(
-        JSXSlack(
-          <Blocks>
-            <Section>Nested</Section>
-            <Section>fragments</Section>
-            <Section>with</Section>
-            <Section>multiple</Section>
-            <Section>components</Section>
-            <Section>are</Section>
-            <Section>supported</Section>
-            <Section>well</Section>
-          </Blocks>
-        )
-      )
-    })
+    expect(onParsed).toBeCalledTimes(1)
   })
 
-  describe('<SelectFragment> component', () => {
-    it('allows building object for external data source of <ExternalSelect>', () => {
-      const expectedOptions: Required<Pick<StaticSelect, 'options'>> = {
-        options: [
-          {
-            text: { type: 'plain_text', text: 'A', emoji: true },
-            value: 'a',
-          },
-          {
-            text: { type: 'plain_text', text: 'B', emoji: true },
-            value: 'b',
-          },
-          {
-            text: { type: 'plain_text', text: 'C', emoji: true },
-            value: 'c',
-          },
-        ],
-      }
-
-      expect(
-        JSXSlack(
-          <SelectFragment>
-            <Option value="a">A</Option>
-            <Option value="b">B</Option>
-            <Option value="c">C</Option>
-          </SelectFragment>
-        )
-      ).toStrictEqual(expectedOptions)
-
-      expect(
-        JSXSlack(
-          <SelectFragment>
-            <option value="a">A</option>
-            <option value="b">B</option>
-            <option value="c">C</option>
-          </SelectFragment>
-        )
-      ).toStrictEqual(expectedOptions)
-
-      const expectedOptgroups: Required<Pick<StaticSelect, 'option_groups'>> = {
-        option_groups: [
-          {
-            label: { type: 'plain_text', text: 'A', emoji: true },
-            options: [
-              {
-                text: { type: 'plain_text', text: 'one', emoji: true },
-                value: '1',
-              },
-              {
-                text: { type: 'plain_text', text: 'two', emoji: true },
-                value: '2',
-              },
-            ],
-          },
-          {
-            label: { type: 'plain_text', text: 'B', emoji: true },
-            options: [
-              {
-                text: { type: 'plain_text', text: 'three', emoji: true },
-                value: '3',
-              },
-              {
-                text: { type: 'plain_text', text: 'four', emoji: true },
-                value: '4',
-              },
-            ],
-          },
-        ],
-      }
-
-      expect(
-        JSXSlack(
-          <SelectFragment>
-            <Optgroup label="A">
-              <Option value="1">one</Option>
-              <Option value="2">two</Option>
-            </Optgroup>
-            <Optgroup label="B">
-              <Option value="3">three</Option>
-              <Option value="4">four</Option>
-            </Optgroup>
-          </SelectFragment>
-        )
-      ).toStrictEqual(expectedOptgroups)
-
-      expect(
-        JSXSlack(
-          <SelectFragment>
-            <optgroup label="A">
-              <option value="1">one</option>
-              <option value="2">two</option>
-            </optgroup>
-            <optgroup label="B">
-              <option value="3">three</option>
-              <option value="4">four</option>
-            </optgroup>
-          </SelectFragment>
-        )
-      ).toStrictEqual(expectedOptgroups)
-    })
-
-    it('allows no options to return empty result', () => {
-      expect(JSXSlack(<SelectFragment />)).toStrictEqual({
-        options: [],
-      })
-
-      expect(JSXSlack(<SelectFragment>{}</SelectFragment>)).toStrictEqual({
-        options: [],
-      })
-    })
-  })
+  it('throws error by passed invalid node', () =>
+    expect(() => JSXSlack({ props: {}, type: -1 } as any)).toThrow())
 })