setQuillEditor(event) {
     this.boardService.setQuillEditor(event);
   }


shareBoard() {
     if(this.boardService.isOwner()) {
       this.boardService.unfocusQuillEditor();
       this.dialog.closeAll();
       this.boardService
       .getMembers()
       .subscribe(data => {
         console.log(data.userIds);
          this.dialog
         .open(BoardShareDialogComponent, {width: "55vw", data: data.userIds})
         .afterClosed().subscribe(result => {
                if(result && result.length !== 0) {
                  console.log(data.userIds, result);
                  if(data.userIds.indexOf(result) < 0 && data.userIds.indexOf(this.boardService.getOwnerEmail()) < 0) {
                    this.boardService
                    .shareBoard(result)
                    .subscribe(data =>{},error=>{});
                  }
                }
         });
       }, error => {});

     }
   }


changeBoard() {
     this.boardService.unfocusQuillEditor();
     this.dialog.closeAll();
     this.dialog
     .open(BoardListDialogComponent, {width: "55vw"})
     .afterClosed().subscribe(result => {
          if(result && result.name !== this.boardName) {
            this.boardService
            .getBoard(result.ownerEmail, result.name)
            .subscribe(data => {
              this.zone.run(() => {
               this.boardName = this.boardService.getCurrentBoardName();
               this.textDocumentName = this.boardService.getCurrentTextDocumentName();
              });
            }, error => {});
          }
          console.log(result);
     });
   }


showRoadMap() {
     this.boardService.unfocusQuillEditor();
     this.dialog.closeAll();
     this.dialog
     .open(RoadMapDialogComponent, {width: "65vw"})
     .afterClosed().subscribe(result => {
          console.log(result);
     });
   }


newBoard() {
     this.boardService.unfocusQuillEditor();
     this.dialog.closeAll();
     this.dialog
     .open(BoardNewDialogComponent)
     .afterClosed().subscribe(result => {
       if(result) {
         this.boardService
         .createBoard(result)
         .subscribe(data => {
           this.zone.run(() => {
             this.boardName = this.boardService.getCurrentBoardName();
             this.textDocumentName = this.boardService.getCurrentTextDocumentName();
           });
         }, error => {});
       }
     });
   }


newTextDocument() {
     this.boardService.unfocusQuillEditor();
     this.dialog.closeAll();
     this.dialog
     .open(TextDocumentNewDialogComponent)
     .afterClosed().subscribe(result => {
       if(result) {
         this.boardService
         .createTextDocument(result)
         .subscribe(data => {
           this.zone.run(() => {
             console.log(this.boardService.getCurrentTextDocumentName());
             this.textDocumentName = this.boardService.getCurrentTextDocumentName();
           });
         }, error => {});
       }
     });
   }


changeTextDocument() {
     this.boardService.unfocusQuillEditor();
     this.dialog.closeAll();
     this.dialog
     .open(TextDocumentListDialogComponent, {width: "55vw"})
     .afterClosed().subscribe(result => {
          if(result && result.name !== this.boardName) {
            this.boardService
            .getTextDocument(result.ownerEmail, result.name)
            .subscribe(data => {
              this.zone.run(() => {
                console.log(this.boardService.getCurrentTextDocumentName());
               this.textDocumentName = this.boardService.getCurrentTextDocumentName();
              });
            }, error => {});
          }
          console.log(result);
     });
   }


showChangeBoardName() {
     if(!this.boardService.isOwner() || this.boardService.isShared()) {
       return;
     }
     this.zone.run(() => {
       this.newBoardName = this.boardName;
       this.showChangeBoardNameInput = true;
       this.boardNameTimeoutId = setTimeout(() => { this.showChangeBoardNameInput = false}, 2000);
     });
   }


showChangeTextDocumentName() {
     this.zone.run(() => {
       this.newTextDocumentName = this.textDocumentName;
       this.showChangeTextDocumentNameInput = true;
       this.textDocumentNameTimeoutId = setTimeout(() => { this.showChangeTextDocumentNameInput = false}, 2000);
     });
   }


focusedBoardNameInput() {
     clearTimeout(this.boardNameTimeoutId);
   }


focusedTextDocumentNameInput() {
     clearTimeout(this.textDocumentNameTimeoutId);
   }


changeBoardName() {
     if(this.newBoardName !== this.boardName &&
        !this.boardService.isShared()) {
       this.boardService
       .changeBoardName(this.newBoardName)
       .subscribe(data => {
           this.boardName = this.newBoardName;
       }, error => {});
     }
     this.showChangeBoardNameInput = false;
   }


changeTextDocumentName() {
     if(this.newTextDocumentName !== this.textDocumentName) {
       this.boardService
       .changeTextDocumentName(this.newTextDocumentName)
       .subscribe(data => {
           this.textDocumentName = this.newTextDocumentName;
       }, error => {});
     }
     this.showChangeTextDocumentNameInput = false;
   }


hideBoardNameInput() {
     console.log('lost');
     this.showChangeBoardNameInput = false;
   }


hideTextDocumentNameInput() {
     console.log('lost');
     this.showChangeTextDocumentNameInput = false;
   }


clearAndLogout() {
     console.log('here');
     this.dialog.closeAll();
     this.boardService.clearAndLogout();
     this.router.navigateByUrl('/login');
   }


class HeroesContainerCard extends Component<Props> {
    render() {
        return (
            <Link href={"/heroes/" + this.props.subClassName} passHref>

                    <Image
                        src={this.props.imgSrc}
                        alt="img of class"
                        height="200"
                        width="200"
                        placeholder="blur"
                        className={styles.image}
                    ></Image>

                            {this.props.subClassNameDisplay}

            </Link>
        );
    }
}


interface Props {
    class: string;
    subClassName: string;
    subClassNameDisplay: string;
    primaryStat: string;
    imgSrc: StaticImageData;
}


render() {
        return (
            <Link href={"/heroes/" + this.props.subClassName} passHref>

                    <Image
                        src={this.props.imgSrc}
                        alt="img of class"
                        height="200"
                        width="200"
                        placeholder="blur"
                        className={styles.image}
                    ></Image>

                            {this.props.subClassNameDisplay}

            </Link>
        );
    }


() => {
            button.dispose();
        }


(sender) => {
        }


() => {
            if (this.cellsIndex === undefined) {
                alert('You should start the NB2Slide widget to use this function')
                return
            }
            const activeCell = this.notebookTools.activeNotebookPanel.content.activeCell
            let cellOutput = null
            try {
                cellOutput = getOutputAreaElements(activeCell.node).output_arr[0].item(0).getElementsByTagName('img')[0].currentSrc;
            }
            catch(error) {
                alert('Do not find image type output! Only support image-based output migration now!')
                return
            }
            this.widgetContent.setClipboard(cellOutput)
            this.widgetContent.update()
            this.widget.update()
        }


(args: any) => {
                //@ts-ignore
                const cells: Array<Cell> = this.notebookTools.activeNotebookPanel.content.widgets
                this.notebookPanel = this.notebookTools.activeNotebookPanel
                this.cellsIndex = computeCurCellsIdx(cells)

                // configure the widget
                this.widgetContent.setNotebookCells(bundleStaticNotebookCells(cells))

                const navNBCallback = (cellIdx: number) => {
                    this.notebookPanel.content.activeCellIndex = cellIdx
                    this.cellsIndex[cellIdx].node.scrollIntoView();
                }

                const getNBCell = (cellIdx: number) => {
                    return this.cellsIndex[cellIdx]
                }

                this.widgetContent.setNavNBCb(navNBCallback)
                this.widgetContent.setGetNBCell(getNBCell)

                // Update the React widget
                this.widgetContent.update()
                this.widget.update()
                if (!this.widget.isAttached) {
                    // Attach the widget to the main work area if it's not there
                    this.app.shell.add(this.widget, 'right');
                }
                // Activate the widget
                this.app.shell.activateById(this.widget.id);
            }


(cellIdx: number) => {
                    this.notebookPanel.content.activeCellIndex = cellIdx
                    this.cellsIndex[cellIdx].node.scrollIntoView();
                }


(cellIdx: number) => {
                    return this.cellsIndex[cellIdx]
                }


class OutputCatchBtnExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
    onclick: () => void

    constructor(onclick: () => void) {
        this.onclick = onclick
    }

    createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
        let button = new ToolbarButton({
            className: 'outputCatch',
            iconClass: 'far fa-object-group',
            onClick: this.onclick,
            tooltip: 'Copy Output'
        });

        panel.toolbar.insertItem(0, 'copyOuput', button);
        return new DisposableDelegate(() => {
            button.dispose();
        });
    }
}


// class CellBindingBtnExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
//     onclick: () => void
//     constructor(onclick: () => void) {
//         this.onclick = onclick
//     }
//     createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
//         let button = new ToolbarButton({
//             className: 'cellBinding',
//             iconClass: 'fas fa-link',
//             onClick: this.onclick,
//             tooltip: 'Bind the current code cell with the current selected slide'
//         });
//         panel.toolbar.insertItem(0, 'cellBinding', button);
//         return new DisposableDelegate(() => {
//             button.dispose();
//         });
//     }
// }
export class NB2Slide {
    app: JupyterFrontEnd;
    tracker: INotebookTracker;
    menu: IMainMenu;
    notebookTools: INotebookTools;
    widgetContent: SlideViewWidget;
    widget: MainAreaWidget;
    command: string;
    notebookPanel: NotebookPanel;
    cellsIndex: { [idx: number]: Cell }

    constructor(
        app: JupyterFrontEnd,
        mainmenu: IMainMenu,
        notebook: INotebookTools | null,
        notebook_tracker: INotebookTracker
    ) {
        this.app = app;
        this.tracker = notebook_tracker;
        this.menu = mainmenu;
        this.notebookTools = notebook
        this.command = ""

        // set up widget
        this.widgetContent = new SlideViewWidget();
        this.widgetContent.addClass('nb2slide-Widget');
        this.widget = new MainAreaWidget<SlideViewWidget>({
            content: this.widgetContent
        });
        this.widget.id = 'nb2slide-jupyterlab';
        this.widget.title.label = 'Slide Decks';
        this.widget.title.closable = true;

        // activate
        this.addTrigger()
        this.addToMenu()
        this.tracker.activeCellChanged.connect((sender) => {
        }, this);
    }

    addTrigger(): void {
        const { commands } = this.app;
        this.command = 'jlab-examples:command-palette';

        // const bindOnClick = () => {
        //     if (this.cellsIndex === undefined) {
        //         alert('You should start the NB2Slide widget to use this function')
        //         return
        //     }
        //     const cellIdx = this.notebookTools.activeNotebookPanel.content.activeCellIndex
        //     this.widgetContent.setBindCellIdx(cellIdx)
        //     this.widgetContent.update()
        //     this.widget.update()
        // }

        const copyOutputOnClick = () => {
            if (this.cellsIndex === undefined) {
                alert('You should start the NB2Slide widget to use this function')
                return
            }
            const activeCell = this.notebookTools.activeNotebookPanel.content.activeCell
            let cellOutput = null
            try {
                cellOutput = getOutputAreaElements(activeCell.node).output_arr[0].item(0).getElementsByTagName('img')[0].currentSrc;
            }
            catch(error) {
                alert('Do not find image type output! Only support image-based output migration now!')
                return
            }
            this.widgetContent.setClipboard(cellOutput)
            this.widgetContent.update()
            this.widget.update()
        }

        this.app.docRegistry.addWidgetExtension('Notebook', new OutputCatchBtnExtension(copyOutputOnClick));
        // this.app.docRegistry.addWidgetExtension('Notebook', new CellBindingBtnExtension(bindOnClick));

        // Add a command
        commands.addCommand(this.command, {
            label: 'Generate slides',
            caption: 'Generate slides',
            execute: (args: any) => {
                //@ts-ignore
                const cells: Array<Cell> = this.notebookTools.activeNotebookPanel.content.widgets
                this.notebookPanel = this.notebookTools.activeNotebookPanel
                this.cellsIndex = computeCurCellsIdx(cells)

                // configure the widget
                this.widgetContent.setNotebookCells(bundleStaticNotebookCells(cells))

                const navNBCallback = (cellIdx: number) => {
                    this.notebookPanel.content.activeCellIndex = cellIdx
                    this.cellsIndex[cellIdx].node.scrollIntoView();
                }

                const getNBCell = (cellIdx: number) => {
                    return this.cellsIndex[cellIdx]
                }

                this.widgetContent.setNavNBCb(navNBCallback)
                this.widgetContent.setGetNBCell(getNBCell)

                // Update the React widget
                this.widgetContent.update()
                this.widget.update()
                if (!this.widget.isAttached) {
                    // Attach the widget to the main work area if it's not there
                    this.app.shell.add(this.widget, 'right');
                }
                // Activate the widget
                this.app.shell.activateById(this.widget.id);
            }
        });
    }

    addToMenu(): void {
        const slideMenu: Menu = new Menu({ commands: this.app.commands })
        slideMenu.title.label = "Notebook2slides"
        this.menu.addMenu(slideMenu, { rank: 80 })
        slideMenu.addItem({
            command: this.command,
            args: {
                origin: "generate"
            }
        })
    }
}


createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
        let button = new ToolbarButton({
            className: 'outputCatch',
            iconClass: 'far fa-object-group',
            onClick: this.onclick,
            tooltip: 'Copy Output'
        });

        panel.toolbar.insertItem(0, 'copyOuput', button);
        return new DisposableDelegate(() => {
            button.dispose();
        });
    }


addTrigger(): void {
        const { commands } = this.app;
        this.command = 'jlab-examples:command-palette';

        // const bindOnClick = () => {
        //     if (this.cellsIndex === undefined) {
        //         alert('You should start the NB2Slide widget to use this function')
        //         return
        //     }
        //     const cellIdx = this.notebookTools.activeNotebookPanel.content.activeCellIndex
        //     this.widgetContent.setBindCellIdx(cellIdx)
        //     this.widgetContent.update()
        //     this.widget.update()
        // }

        const copyOutputOnClick = () => {
            if (this.cellsIndex === undefined) {
                alert('You should start the NB2Slide widget to use this function')
                return
            }
            const activeCell = this.notebookTools.activeNotebookPanel.content.activeCell
            let cellOutput = null
            try {
                cellOutput = getOutputAreaElements(activeCell.node).output_arr[0].item(0).getElementsByTagName('img')[0].currentSrc;
            }
            catch(error) {
                alert('Do not find image type output! Only support image-based output migration now!')
                return
            }
            this.widgetContent.setClipboard(cellOutput)
            this.widgetContent.update()
            this.widget.update()
        }

        this.app.docRegistry.addWidgetExtension('Notebook', new OutputCatchBtnExtension(copyOutputOnClick));
        // this.app.docRegistry.addWidgetExtension('Notebook', new CellBindingBtnExtension(bindOnClick));

        // Add a command
        commands.addCommand(this.command, {
            label: 'Generate slides',
            caption: 'Generate slides',
            execute: (args: any) => {
                //@ts-ignore
                const cells: Array<Cell> = this.notebookTools.activeNotebookPanel.content.widgets
                this.notebookPanel = this.notebookTools.activeNotebookPanel
                this.cellsIndex = computeCurCellsIdx(cells)

                // configure the widget
                this.widgetContent.setNotebookCells(bundleStaticNotebookCells(cells))

                const navNBCallback = (cellIdx: number) => {
                    this.notebookPanel.content.activeCellIndex = cellIdx
                    this.cellsIndex[cellIdx].node.scrollIntoView();
                }

                const getNBCell = (cellIdx: number) => {
                    return this.cellsIndex[cellIdx]
                }

                this.widgetContent.setNavNBCb(navNBCallback)
                this.widgetContent.setGetNBCell(getNBCell)

                // Update the React widget
                this.widgetContent.update()
                this.widget.update()
                if (!this.widget.isAttached) {
                    // Attach the widget to the main work area if it's not there
                    this.app.shell.add(this.widget, 'right');
                }
                // Activate the widget
                this.app.shell.activateById(this.widget.id);
            }
        });
    }


addToMenu(): void {
        const slideMenu: Menu = new Menu({ commands: this.app.commands })
        slideMenu.title.label = "Notebook2slides"
        this.menu.addMenu(slideMenu, { rank: 80 })
        slideMenu.addItem({
            command: this.command,
            args: {
                origin: "generate"
            }
        })
    }


export enum LobbyStatusEnum {
  AVAILABLE,
  BUSY,
  DONE,
}


function useSelectedItemsState({
  defaultSelectedKeys,
  selectedKeys,
  defaultSelectedKey,
  selectedKey,
  options,
  multiSelect,
}: IDropdownProps) {
  const oldOptions = usePrevious(options);
  const [selectedIndices, setSelectedIndices] = React.useState<number[]>([]);

  // In controlled component usage where selectedKey is provided, update the selectedIndex
  // state if the key or options change.
  let selectedKeyPropToUse: string | number | string[] | number[] | null | undefined;

  // this does a shallow compare (assumes options are pure), for the purposes of determining whether
  // defaultSelectedKey/defaultSelectedKeys are respected.
  const didOptionsChange = options !== oldOptions;

  if (multiSelect) {
    if (didOptionsChange && defaultSelectedKeys !== undefined) {
      selectedKeyPropToUse = defaultSelectedKeys;
    } else {
      selectedKeyPropToUse = selectedKeys;
    }
  } else {
    if (didOptionsChange && defaultSelectedKey !== undefined) {
      selectedKeyPropToUse = defaultSelectedKey;
    } else {
      selectedKeyPropToUse = selectedKey;
    }
  }

  const oldSelectedKeyProp = usePrevious(selectedKeyPropToUse);

  React.useEffect(() => {
    /** Get all selected indexes for multi-select mode */
    const getSelectedIndexes = (): number[] => {
      if (selectedKeyPropToUse === undefined) {
        if (multiSelect) {
          return getAllSelectedIndices();
        }
        const selectedIndex = getSelectedIndex(null);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      } else if (!Array.isArray(selectedKeyPropToUse)) {
        const selectedIndex = getSelectedIndex(selectedKeyPropToUse);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      }

      const returnValue: number[] = [];
      for (const key of selectedKeyPropToUse) {
        const selectedIndex = getSelectedIndex(key);
        selectedIndex !== -1 && returnValue.push(selectedIndex);
      }
      return returnValue;
    };

    const getAllSelectedIndices = (): number[] => {
      return options
        .map((option: IDropdownOption, index: number) => (option.selected ? index : -1))
        .filter(index => index !== -1);
    };

    const getSelectedIndex = (searchKey: string | number | null | undefined): number => {
      return findIndex(options, option => {
        // eslint-disable-next-line eqeqeq
        if (searchKey != null) {
          return option.key === searchKey;
        } else {
          // eslint-disable-next-line deprecation/deprecation
          return !!option.selected || !!option.isSelected;
        }
      });
    };

    if (
      (selectedKeyPropToUse !== undefined || !oldOptions) &&
      (selectedKeyPropToUse !== oldSelectedKeyProp || didOptionsChange)
    ) {
      setSelectedIndices(getSelectedIndexes());
    }
  }, [didOptionsChange, multiSelect, oldOptions, oldSelectedKeyProp, options, selectedKeyPropToUse]);

  return [selectedIndices, setSelectedIndices] as const;
}


() => {
    /** Get all selected indexes for multi-select mode */
    const getSelectedIndexes = (): number[] => {
      if (selectedKeyPropToUse === undefined) {
        if (multiSelect) {
          return getAllSelectedIndices();
        }
        const selectedIndex = getSelectedIndex(null);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      } else if (!Array.isArray(selectedKeyPropToUse)) {
        const selectedIndex = getSelectedIndex(selectedKeyPropToUse);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      }

      const returnValue: number[] = [];
      for (const key of selectedKeyPropToUse) {
        const selectedIndex = getSelectedIndex(key);
        selectedIndex !== -1 && returnValue.push(selectedIndex);
      }
      return returnValue;
    };

    const getAllSelectedIndices = (): number[] => {
      return options
        .map((option: IDropdownOption, index: number) => (option.selected ? index : -1))
        .filter(index => index !== -1);
    };

    const getSelectedIndex = (searchKey: string | number | null | undefined): number => {
      return findIndex(options, option => {
        // eslint-disable-next-line eqeqeq
        if (searchKey != null) {
          return option.key === searchKey;
        } else {
          // eslint-disable-next-line deprecation/deprecation
          return !!option.selected || !!option.isSelected;
        }
      });
    };

    if (
      (selectedKeyPropToUse !== undefined || !oldOptions) &&
      (selectedKeyPropToUse !== oldSelectedKeyProp || didOptionsChange)
    ) {
      setSelectedIndices(getSelectedIndexes());
    }
  }


(): number[] => {
      if (selectedKeyPropToUse === undefined) {
        if (multiSelect) {
          return getAllSelectedIndices();
        }
        const selectedIndex = getSelectedIndex(null);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      } else if (!Array.isArray(selectedKeyPropToUse)) {
        const selectedIndex = getSelectedIndex(selectedKeyPropToUse);
        return selectedIndex !== -1 ? [selectedIndex] : [];
      }

      const returnValue: number[] = [];
      for (const key of selectedKeyPropToUse) {
        const selectedIndex = getSelectedIndex(key);
        selectedIndex !== -1 && returnValue.push(selectedIndex);
      }
      return returnValue;
    }


(): number[] => {
      return options
        .map((option: IDropdownOption, index: number) => (option.selected ? index : -1))
        .filter(index => index !== -1);
    }


(option: IDropdownOption, index: number) => (option.selected ? index : -1)


index => index !== -1


(searchKey: string | number | null | undefined): number => {
      return findIndex(options, option => {
        // eslint-disable-next-line eqeqeq
        if (searchKey != null) {
          return option.key === searchKey;
        } else {
          // eslint-disable-next-line deprecation/deprecation
          return !!option.selected || !!option.isSelected;
        }
      });
    }


option => {
        // eslint-disable-next-line eqeqeq
        if (searchKey != null) {
          return option.key === searchKey;
        } else {
          // eslint-disable-next-line deprecation/deprecation
          return !!option.selected || !!option.isSelected;
        }
      }


(propsWithoutDefaults, forwardedRef) => {
    const props = getPropsWithDefaults(DEFAULT_PROPS, propsWithoutDefaults);

    const rootRef = React.useRef<HTMLDivElement>(null);
    const mergedRootRef = useMergedRefs(forwardedRef, rootRef);

    const responsiveMode = useResponsiveMode(rootRef, props.responsiveMode);
    const [selectedIndices, setSelectedIndices] = useSelectedItemsState(props);

    return (
      <DropdownInternal
        {...(props as Omit<IDropdownProps, 'ref'>)}
        responsiveMode={responsiveMode}
        hoisted={{ rootRef: mergedRootRef, selectedIndices, setSelectedIndices }}


(prop: keyof IDropdownProps) =>
          warn(`Dropdown property '${prop}' cannot be used when 'multiSelect' is true. Use '${prop}s' instead.`)


(prop: keyof IDropdownProps) =>
          warn(`Dropdown property '${prop}s' cannot be used when 'multiSelect' is false/unset. Use '${prop}' instead.`)


(
    event: React.FormEvent<HTMLDivElement>,
    options: IDropdownOption[],
    index: number,
    checked?: boolean,
    multiSelect?: boolean,
  ) => {
    // eslint-disable-next-line deprecation/deprecation
    const { onChange, onChanged } = this.props;
    if (onChange || onChanged) {
      // for single-select, option passed in will always be selected.
      // for multi-select, flip the checked value
      const changedOpt = multiSelect ? { ...options[index], selected: !checked } : options[index];

      onChange && onChange({ ...event, target: this._dropDown.current as EventTarget }, changedOpt, index);
      onChanged && onChanged(changedOpt, index);
    }
  }


(): string | undefined => {
    // eslint-disable-next-line deprecation/deprecation
    return this.props.placeholder || this.props.placeHolder;
  }


(items: IDropdownOption[], _unused?: unknown): string => {
    const { multiSelectDelimiter = ', ' } = this.props;
    return items.map(i => i.text).join(multiSelectDelimiter);
  }


i => i.text


(items: IDropdownOption[]): JSX.Element => {
    return <>{this._getTitle(items)}</>;
  }


(props: IDropdownProps): JSX.Element | null => {
    if (!this._getPlaceholder()) {
      return null;
    }
    return <>{this._getPlaceholder()}</>;
  }


(props: ISelectableDroppableTextProps<IDropdown, HTMLDivElement>): JSX.Element => {
    const { calloutProps, panelProps } = props;
    const { responsiveMode, dropdownWidth } = this.props;

    const isSmall = responsiveMode! <= ResponsiveMode.medium;

    const panelStyles = this._classNames.subComponentStyles
      ? (this._classNames.subComponentStyles.panel as IStyleFunctionOrObject<IPanelStyleProps, IPanelStyles>)
      : undefined;

    let calloutWidth = undefined;
    let calloutMinWidth = undefined;
    if (dropdownWidth === 'auto') {
      calloutMinWidth = this._dropDown.current ? this._dropDown.current.clientWidth : 0;
    } else {
      calloutWidth = dropdownWidth || (this._dropDown.current ? this._dropDown.current.clientWidth : 0);
    }

    return isSmall ? (
      <Panel
        isOpen={true}
        isLightDismiss={true}
        onDismiss={this._onDismiss}
        hasCloseButton={false}
        styles={panelStyles}
        {...panelProps}
      >
        {this._renderFocusableList(props)}
      </Panel>
    ) : (
      <Callout
        isBeakVisible={false}
        gapSpace={0}
        doNotLayer={false}
        directionalHintFixed={false}
        directionalHint={DirectionalHint.bottomLeftEdge}
        calloutWidth={calloutWidth}
        calloutMinWidth={calloutMinWidth}
        {...calloutProps}
        className={this._classNames.callout}
        target={this._dropDown.current}
        onDismiss={this._onDismiss}
        onScroll={this._onScroll}
        onPositioned={this._onPositioned}
      >
        {this._renderFocusableList(props)}
      </Callout>
    );
  }


(props: IDropdownProps): JSX.Element => {
    return <Icon className={this._classNames.caretDown} iconName="ChevronDown" aria-hidden={true} />;
  }


(props: ISelectableDroppableTextProps<IDropdown, HTMLDivElement>): JSX.Element => {
    const { onRenderItem = this._onRenderItem } = props;

    let queue: { id?: string; items: JSX.Element[] } = { items: [] };
    let renderedList: JSX.Element[] = [];

    const emptyQueue = (): void => {
      const newGroup = queue.id
        ? [
            <div role="group" key={queue.id} aria-labelledby={queue.id}>
              {queue.items}
            </div>,
          ]
        : queue.items;

      renderedList = [...renderedList, ...newGroup];
      // Flush items and id
      queue = { items: [] };
    };

    const placeRenderedOptionIntoQueue = (item: IDropdownOption, index: number) => {
      /*
        Case Header
          empty queue if it's not already empty
          ensure unique ID for header and set queue ID
          push header into queue
        Case Divider
          push divider into queue if not first item
          empty queue if not already empty
        Default
          push item into queue
      */
      switch (item.itemType) {
        case SelectableOptionMenuItemType.Header:
          queue.items.length > 0 && emptyQueue();

          const id = this._id + item.key;
          queue.items.push(onRenderItem({ id, ...item, index }, this._onRenderItem)!);
          queue.id = id;
          break;
        case SelectableOptionMenuItemType.Divider:
          index > 0 && queue.items.push(onRenderItem({ ...item, index }, this._onRenderItem)!);

          queue.items.length > 0 && emptyQueue();
          break;
        default:
          queue.items.push(onRenderItem({ ...item, index }, this._onRenderItem)!);
      }
    };

    // Place options into the queue. Queue will be emptied anytime a Header or Divider is encountered
    props.options.forEach((item: IDropdownOption, index: number) => {
      placeRenderedOptionIntoQueue(item, index);
    });

    // Push remaining items into all renderedList
    queue.items.length > 0 && emptyQueue();

    return <>{renderedList}</>;
  }


(): void => {
      const newGroup = queue.id
        ? [
            <div role="group" key={queue.id} aria-labelledby={queue.id}>
              {queue.items}
            </div>,
          ]
        : queue.items;

      renderedList = [...renderedList, ...newGroup];
      // Flush items and id
      queue = { items: [] };
    }


(item: IDropdownOption, index: number) => {
      /*
        Case Header
          empty queue if it's not already empty
          ensure unique ID for header and set queue ID
          push header into queue
        Case Divider
          push divider into queue if not first item
          empty queue if not already empty
        Default
          push item into queue
      */
      switch (item.itemType) {
        case SelectableOptionMenuItemType.Header:
          queue.items.length > 0 && emptyQueue();

          const id = this._id + item.key;
          queue.items.push(onRenderItem({ id, ...item, index }, this._onRenderItem)!);
          queue.id = id;
          break;
        case SelectableOptionMenuItemType.Divider:
          index > 0 && queue.items.push(onRenderItem({ ...item, index }, this._onRenderItem)!);

          queue.items.length > 0 && emptyQueue();
          break;
        default:
          queue.items.push(onRenderItem({ ...item, index }, this._onRenderItem)!);
      }
    }


(item: IDropdownOption, index: number) => {
      placeRenderedOptionIntoQueue(item, index);
    }


(item: IDropdownOption): JSX.Element | null => {
    switch (item.itemType) {
      case SelectableOptionMenuItemType.Divider:
        return this._renderSeparator(item);
      case SelectableOptionMenuItemType.Header:
        return this._renderHeader(item);
      default:
        return this._renderOption(item);
    }
  }


(item: IDropdownOption): JSX.Element => {
    const {
      onRenderOption = this._onRenderOption,
      hoisted: { selectedIndices = [] },
    } = this.props;
    const isItemSelected =
      item.index !== undefined && selectedIndices ? selectedIndices.indexOf(item.index) > -1 : false;

    // select the right className based on the combination of selected/disabled
    const itemClassName = item.hidden // predicate: item hidden
      ? this._classNames.dropdownItemHidden
      : isItemSelected && item.disabled === true // predicate: both selected and disabled
      ? this._classNames.dropdownItemSelectedAndDisabled
      : isItemSelected // predicate: selected only
      ? this._classNames.dropdownItemSelected
      : item.disabled === true // predicate: disabled only
      ? this._classNames.dropdownItemDisabled
      : this._classNames.dropdownItem;

    const { title } = item;

    const multiSelectItemStyles = this._classNames.subComponentStyles
      ? (this._classNames.subComponentStyles.multiSelectItem as IStyleFunctionOrObject<
          ICheckboxStyleProps,
          ICheckboxStyles
        >)
      : undefined;

    return !this.props.multiSelect ? (
      <CommandButton
        id={this._listId + item.index}
        key={item.key}
        data-index={item.index}
        data-is-focusable={!item.disabled}
        disabled={item.disabled}
        className={itemClassName}
        onClick={this._onItemClick(item)}
        // eslint-disable-next-line react/jsx-no-bind
        onMouseEnter={this._onItemMouseEnter.bind(this, item)}
        // eslint-disable-next-line react/jsx-no-bind
        onMouseLeave={this._onMouseItemLeave.bind(this, item)}
        // eslint-disable-next-line react/jsx-no-bind
        onMouseMove={this._onItemMouseMove.bind(this, item)}
        role="option"
        aria-selected={isItemSelected ? 'true' : 'false'}
        ariaLabel={item.ariaLabel}
        title={title}
        aria-posinset={this._sizePosCache.positionInSet(item.index)}
        aria-setsize={this._sizePosCache.optionSetSize}
      >
        {onRenderOption(item, this._onRenderOption)}
      </CommandButton>
    ) : (
      <Checkbox
        id={this._listId + item.index}
        key={item.key}
        disabled={item.disabled}
        onChange={this._onItemClick(item)}
        inputProps={{
          'aria-selected': isItemSelected,
          onMouseEnter: this._onItemMouseEnter.bind(this, item),
          onMouseLeave: this._onMouseItemLeave.bind(this, item),
          onMouseMove: this._onItemMouseMove.bind(this, item),
          role: 'option',
          ...({
            'data-index': item.index,
            'data-is-focusable': !item.disabled,
          } as any),
        }}


(item: IDropdownOption): JSX.Element => {
    return {item.text};
  }


(item: IDropdownOption): JSX.Element | null => {
    const { onRenderOption = this._onRenderOption } = this.props;
    return onRenderOption(item, this._onRenderOption);
  }


(positions?: ICalloutPositionedInfo): void => {
    if (this._focusZone.current) {
      // Focusing an element can trigger a reflow. Making this wait until there is an animation
      // frame can improve perf significantly.
      this._requestAnimationFrame(() => {
        const selectedIndices = this.props.hoisted.selectedIndices;
        if (this._focusZone.current) {
          if (selectedIndices && selectedIndices[0] && !this.props.options[selectedIndices[0]].disabled) {
            const element: HTMLElement | null = getDocument()!.getElementById(`${this._id}-list${selectedIndices[0]}`);
            if (element) {
              this._focusZone.current.focusElement(element);
            }
          } else {
            this._focusZone.current.focus();
          }
        }
      });
    }

    if (!this.state.calloutRenderEdge || this.state.calloutRenderEdge !== positions!.targetEdge) {
      this.setState({
        calloutRenderEdge: positions!.targetEdge,
      });
    }
  }


() => {
        const selectedIndices = this.props.hoisted.selectedIndices;
        if (this._focusZone.current) {
          if (selectedIndices && selectedIndices[0] && !this.props.options[selectedIndices[0]].disabled) {
            const element: HTMLElement | null = getDocument()!.getElementById(`${this._id}-list${selectedIndices[0]}`);
            if (element) {
              this._focusZone.current.focusElement(element);
            }
          } else {
            this._focusZone.current.focus();
          }
        }
      }


(item: IDropdownOption): ((event: React.MouseEvent<HTMLDivElement>) => void) => {
    return (event: React.MouseEvent<HTMLDivElement>): void => {
      if (!item.disabled) {
        this.setSelectedIndex(event, item.index!);
        if (!this.props.multiSelect) {
          // only close the callout when it's in single-select mode
          this.setState({
            isOpen: false,
          });
        }
      }
    };
  }


(event: React.MouseEvent<HTMLDivElement>): void => {
      if (!item.disabled) {
        this.setSelectedIndex(event, item.index!);
        if (!this.props.multiSelect) {
          // only close the callout when it's in single-select mode
          this.setState({
            isOpen: false,
          });
        }
      }
    }


(): void => {
    if (!this._isScrollIdle && this._scrollIdleTimeoutId !== undefined) {
      clearTimeout(this._scrollIdleTimeoutId);
      this._scrollIdleTimeoutId = undefined;
    } else {
      this._isScrollIdle = false;
    }

    this._scrollIdleTimeoutId = window.setTimeout(() => {
      this._isScrollIdle = true;
    }, this._scrollIdleDelay);
  }


(item: any, ev: React.MouseEvent<HTMLElement>): void => {
    if (this._shouldIgnoreMouseEvent()) {
      return;
    }

    /**
     * IE11 focus() method forces parents to scroll to top of element.
     * Edge and IE expose a setActive() function for focusable divs that
     * sets the page focus but does not scroll the parent element.
     */
    if (this._host.current) {
      if ((this._host.current as any).setActive) {
        try {
          (this._host.current as any).setActive();
        } catch (e) {
          /* no-op */
        }
      } else {
        this._host.current.focus();
      }
    }
  }


(): void => {
    this.setState({ isOpen: false });
  }


(ev: React.FocusEvent<HTMLDivElement>): void => {
    // If Dropdown disabled do not proceed with this logic.
    const disabled = this._isDisabled();
    if (disabled) {
      return;
    }

    if (this.state.isOpen) {
      // Do not call onBlur or update focus state when the callout is opened
      return;
    }

    this.setState({ hasFocus: false });

    if (this.props.onBlur) {
      this.props.onBlur(ev);
    }
  }


(ev: React.KeyboardEvent<HTMLDivElement>): void => {
    // If Dropdown disabled do not process any keyboard events.
    const disabled = this._isDisabled();
    if (disabled) {
      return;
    }

    // Take note if we are processing an alt (option) or meta (command) keydown.
    // See comment in _shouldHandleKeyUp for reasoning.
    this._lastKeyDownWasAltOrMeta = this._isAltOrMeta(ev);

    if (this.props.onKeyDown) {
      this.props.onKeyDown(ev);
      if (ev.defaultPrevented) {
        return;
      }
    }

    let newIndex: number | undefined;
    const selectedIndex = this.props.hoisted.selectedIndices.length ? this.props.hoisted.selectedIndices[0] : -1;
    const containsExpandCollapseModifier = ev.altKey || ev.metaKey;
    const isOpen = this.state.isOpen;

    // eslint-disable-next-line deprecation/deprecation
    switch (ev.which) {
      case KeyCodes.enter:
        this.setState({
          isOpen: !isOpen,
        });
        break;

      case KeyCodes.escape:
        if (!isOpen) {
          return;
        }

        this.setState({
          isOpen: false,
        });
        break;

      case KeyCodes.up:
        if (containsExpandCollapseModifier) {
          if (isOpen) {
            this.setState({ isOpen: false });
            break;
          }

          return;
        }
        if (this.props.multiSelect) {
          this.setState({ isOpen: true });
        } else if (!this._isDisabled()) {
          newIndex = this._moveIndex(ev, -1, selectedIndex - 1, selectedIndex);
        }
        break;

      case KeyCodes.down:
        if (containsExpandCollapseModifier) {
          ev.stopPropagation();
          ev.preventDefault();
        }
        if ((containsExpandCollapseModifier && !isOpen) || this.props.multiSelect) {
          this.setState({ isOpen: true });
        } else if (!this._isDisabled()) {
          newIndex = this._moveIndex(ev, 1, selectedIndex + 1, selectedIndex);
        }
        break;

      case KeyCodes.home:
        if (!this.props.multiSelect) {
          newIndex = this._moveIndex(ev, 1, 0, selectedIndex);
        }
        break;

      case KeyCodes.end:
        if (!this.props.multiSelect) {
          newIndex = this._moveIndex(ev, -1, this.props.options.length - 1, selectedIndex);
        }
        break;

      case KeyCodes.space:
        // event handled in _onDropdownKeyUp
        break;

      default:
        return;
    }

    if (newIndex !== selectedIndex) {
      ev.stopPropagation();
      ev.preventDefault();
    }
  }


(ev: React.KeyboardEvent<HTMLDivElement>): void => {
    // If Dropdown disabled do not process any keyboard events.
    const disabled = this._isDisabled();
    if (disabled) {
      return;
    }

    const shouldHandleKey = this._shouldHandleKeyUp(ev);
    const isOpen = this.state.isOpen;

    if (this.props.onKeyUp) {
      this.props.onKeyUp(ev);
      if (ev.defaultPrevented) {
        return;
      }
    }
    // eslint-disable-next-line deprecation/deprecation
    switch (ev.which) {
      case KeyCodes.space:
        this.setState({
          isOpen: !isOpen,
        });
        break;

      default:
        if (shouldHandleKey && isOpen) {
          this.setState({ isOpen: false });
        }
        return;
    }

    ev.stopPropagation();
    ev.preventDefault();
  }


(ev: React.KeyboardEvent<HTMLElement>): void => {
    let elementToFocus;

    // Take note if we are processing an alt (option) or meta (command) keydown.
    // See comment in _shouldHandleKeyUp for reasoning.
    this._lastKeyDownWasAltOrMeta = this._isAltOrMeta(ev);
    const containsExpandCollapseModifier = ev.altKey || ev.metaKey;

    // eslint-disable-next-line deprecation/deprecation
    switch (ev.which) {
      case KeyCodes.up:
        if (containsExpandCollapseModifier) {
          this.setState({ isOpen: false });
        } else {
          if (this._host.current) {
            elementToFocus = getLastFocusable(this._host.current, this._host.current.lastChild as HTMLElement, true);
          }
        }
        break;

      // All directional keystrokes should be canceled when the zone is rendered.
      // This avoids the body scroll from reacting and thus dismissing the dropdown.
      case KeyCodes.home:
      case KeyCodes.end:
      case KeyCodes.pageUp:
      case KeyCodes.pageDown:
        break;

      case KeyCodes.down:
        if (!containsExpandCollapseModifier && this._host.current) {
          elementToFocus = getFirstFocusable(this._host.current, this._host.current.firstChild as HTMLElement, true);
        }
        break;

      case KeyCodes.escape:
        this.setState({ isOpen: false });
        break;

      case KeyCodes.tab:
        this.setState({ isOpen: false });
        return;

      default:
        return;
    }

    if (elementToFocus) {
      elementToFocus.focus();
    }

    ev.stopPropagation();
    ev.preventDefault();
  }


(ev: React.KeyboardEvent<HTMLElement>): void => {
    const shouldHandleKey = this._shouldHandleKeyUp(ev);

    if (shouldHandleKey && this.state.isOpen) {
      this.setState({ isOpen: false });
      ev.preventDefault();
    }
  }


(ev: React.MouseEvent<HTMLDivElement>): void => {
    if (this.props.onClick) {
      this.props.onClick(ev);
      if (ev.defaultPrevented) {
        return;
      }
    }

    const { isOpen } = this.state;
    const disabled = this._isDisabled();

    if (!disabled && !this._shouldOpenOnFocus()) {
      this.setState({
        isOpen: !isOpen,
      });
    }

    this._isFocusedByClick = false; // reset
  }


(): void => {
    this._isFocusedByClick = true;
  }


(ev: React.FocusEvent<HTMLDivElement>): void => {
    const disabled = this._isDisabled();

    if (!disabled) {
      if (this.props.onFocus) {
        this.props.onFocus(ev);
      }
      const state: Pick<IDropdownState, 'hasFocus'> | Pick<IDropdownState, 'hasFocus' | 'isOpen'> = { hasFocus: true };
      if (this._shouldOpenOnFocus()) {
        (state as Pick<IDropdownState, 'hasFocus' | 'isOpen'>).isOpen = true;
      }

      this.setState(state);
    }
  }


() => boolean | undefined


() => {
    let { disabled } = this.props;
    // eslint-disable-next-line deprecation/deprecation
    const { isDisabled } = this.props;

    // Remove this deprecation workaround at 1.0.0
    if (disabled === undefined) {
      disabled = isDisabled;
    }

    return disabled;
  }


(props: IDropdownProps): JSX.Element | null => {
    const { label, required, disabled } = props;

    const labelStyles = this._classNames.subComponentStyles
      ? (this._classNames.subComponentStyles.label as IStyleFunctionOrObject<ILabelStyleProps, ILabelStyles>)
      : undefined;

    return label ? (
      <Label
        className={this._classNames.label}
        id={this._labelId}
        required={required}
        styles={labelStyles}
        disabled={disabled}
      >
        {label}
      </Label>
    ) : null;
  }


class DropdownInternal extends React.Component<IDropdownInternalProps, IDropdownState> implements IDropdown {
  public static defaultProps = {
    options: [] as IDropdownOption[],
  };

  private _host = React.createRef<HTMLDivElement>();
  private _focusZone = React.createRef<FocusZone>();
  private _dropDown = React.createRef<HTMLDivElement>();
  private _id: string;
  private _labelId: string;
  private _listId: string;
  private _optionId: string;
  private _isScrollIdle: boolean;
  private readonly _scrollIdleDelay: number = 250 /* ms */;
  private _scrollIdleTimeoutId: number | undefined;
  /** True if the most recent keydown event was for alt (option) or meta (command). */
  private _lastKeyDownWasAltOrMeta: boolean | undefined;
  private _sizePosCache: DropdownSizePosCache = new DropdownSizePosCache();
  private _classNames: IProcessedStyleSet<IDropdownStyles>;
  private _requestAnimationFrame = safeRequestAnimationFrame(this);
  /** Flag for when we get the first mouseMove */
  private _gotMouseMove: boolean;
  /** Flag for tracking whether focus is triggered by click (alternatively triggered by keyboard nav) */
  private _isFocusedByClick: boolean;

  constructor(props: IDropdownInternalProps) {
    super(props);

    initializeComponentRef(this);

    const { multiSelect, selectedKey, selectedKeys, defaultSelectedKey, defaultSelectedKeys, options } = props;

    if (process.env.NODE_ENV !== 'production') {
      warnDeprecations(COMPONENT_NAME, props, {
        isDisabled: 'disabled',
        onChanged: 'onChange',
        placeHolder: 'placeholder',
        onRenderPlaceHolder: 'onRenderPlaceholder',
      });

      warnMutuallyExclusive(COMPONENT_NAME, props, {
        defaultSelectedKey: 'selectedKey',
        defaultSelectedKeys: 'selectedKeys',
        selectedKeys: 'selectedKey',
      });

      if (multiSelect) {
        const warnMultiSelect = (prop: keyof IDropdownProps) =>
          warn(`Dropdown property '${prop}' cannot be used when 'multiSelect' is true. Use '${prop}s' instead.`);
        if (selectedKey !== undefined) {
          warnMultiSelect('selectedKey');
        }
        if (defaultSelectedKey !== undefined) {
          warnMultiSelect('defaultSelectedKey');
        }
      } else {
        const warnNotMultiSelect = (prop: keyof IDropdownProps) =>
          warn(`Dropdown property '${prop}s' cannot be used when 'multiSelect' is false/unset. Use '${prop}' instead.`);
        if (selectedKeys !== undefined) {
          warnNotMultiSelect('selectedKey');
        }
        if (defaultSelectedKeys !== undefined) {
          warnNotMultiSelect('defaultSelectedKey');
        }
      }
    }

    this._id = props.id || getId('Dropdown');
    this._labelId = this._id + '-label';
    this._listId = this._id + '-list';
    this._optionId = this._id + '-option';
    this._isScrollIdle = true;

    this._sizePosCache.updateOptions(options);

    this.state = {
      isOpen: false,
      hasFocus: false,
      calloutRenderEdge: undefined,
    };
  }

  /**
   * All selected options
   */
  public get selectedOptions(): IDropdownOption[] {
    const {
      options,
      hoisted: { selectedIndices },
    } = this.props;

    return getAllSelectedOptions(options, selectedIndices);
  }

  public componentWillUnmount() {
    clearTimeout(this._scrollIdleTimeoutId);
  }

  public componentDidUpdate(prevProps: IDropdownProps, prevState: IDropdownState) {
    if (prevState.isOpen === true && this.state.isOpen === false) {
      this._gotMouseMove = false;

      if (this.props.onDismiss) {
        this.props.onDismiss();
      }
    }
  }

  public render(): JSX.Element {
    const id = this._id;

    const props = this.props;
    const {
      className,
      label,
      options,
      ariaLabel,
      required,
      errorMessage,
      styles: propStyles,
      theme,
      panelProps,
      calloutProps,
      onRenderTitle = this._getTitle,
      onRenderContainer = this._onRenderContainer,
      onRenderCaretDown = this._onRenderCaretDown,
      onRenderLabel = this._onRenderLabel,
      hoisted: { selectedIndices },
    } = props;
    const { isOpen, calloutRenderEdge } = this.state;
    // eslint-disable-next-line deprecation/deprecation
    const onRenderPlaceholder = props.onRenderPlaceholder || props.onRenderPlaceHolder || this._getPlaceholder;

    // If our cached options are out of date update our cache
    if (options !== this._sizePosCache.cachedOptions) {
      this._sizePosCache.updateOptions(options);
    }

    const selectedOptions = getAllSelectedOptions(options, selectedIndices);
    const divProps = getNativeProps(props, divProperties);

    const disabled = this._isDisabled();

    const errorMessageId = id + '-errorMessage';
    const ariaActiveDescendant = disabled
      ? undefined
      : isOpen && selectedIndices.length === 1 && selectedIndices[0] >= 0
      ? this._listId + selectedIndices[0]
      : undefined;

    this._classNames = getClassNames(propStyles, {
      theme,
      className,
      hasError: !!(errorMessage && errorMessage.length > 0),
      hasLabel: !!label,
      isOpen,
      required,
      disabled,
      isRenderingPlaceholder: !selectedOptions.length,
      panelClassName: panelProps ? panelProps.className : undefined,
      calloutClassName: calloutProps ? calloutProps.className : undefined,
      calloutRenderEdge: calloutRenderEdge,
    });

    const hasErrorMessage: boolean = !!errorMessage && errorMessage.length > 0;

    return (
      <div
        className={this._classNames.root}
        ref={this.props.hoisted.rootRef}
        aria-owns={isOpen ? this._listId : undefined}
      >
        {onRenderLabel(this.props, this._onRenderLabel)}
        <div
          data-is-focusable={!disabled}
          data-ktp-target={true}
          ref={this._dropDown}
          id={id}
          tabIndex={disabled ? -1 : 0}
          role="combobox"
          aria


/** Internal only props interface to support mixing in responsive mode */
// eslint-disable-next-line deprecation/deprecation
interface IDropdownInternalProps extends Omit<IDropdownProps, 'ref'>, IWithResponsiveModeState {
  hoisted: {
    rootRef: React.Ref<HTMLDivElement>;
    selectedIndices: number[];
    setSelectedIndices: React.Dispatch<React.SetStateAction<number[]>>;
  };
}


interface IDropdownState {
  isOpen: boolean;
  /** Used to track whether focus is already within the Dropdown, for openOnFocus handling. */
  hasFocus: boolean;
  calloutRenderEdge?: RectangleEdge;
}


public componentWillUnmount() {
    clearTimeout(this._scrollIdleTimeoutId);
  }


public componentDidUpdate(prevProps: IDropdownProps, prevState: IDropdownState) {
    if (prevState.isOpen === true && this.state.isOpen === false) {
      this._gotMouseMove = false;

      if (this.props.onDismiss) {
        this.props.onDismiss();
      }
    }
  }


public render(): JSX.Element {
    const id = this._id;

    const props = this.props;
    const {
      className,
      label,
      options,
      ariaLabel,
      required,
      errorMessage,
      styles: propStyles,
      theme,
      panelProps,
      calloutProps,
      onRenderTitle = this._getTitle,
      onRenderContainer = this._onRenderContainer,
      onRenderCaretDown = this._onRenderCaretDown,
      onRenderLabel = this._onRenderLabel,
      hoisted: { selectedIndices },
    } = props;
    const { isOpen, calloutRenderEdge } = this.state;
    // eslint-disable-next-line deprecation/deprecation
    const onRenderPlaceholder = props.onRenderPlaceholder || props.onRenderPlaceHolder || this._getPlaceholder;

    // If our cached options are out of date update our cache
    if (options !== this._sizePosCache.cachedOptions) {
      this._sizePosCache.updateOptions(options);
    }

    const selectedOptions = getAllSelectedOptions(options, selectedIndices);
    const divProps = getNativeProps(props, divProperties);

    const disabled = this._isDisabled();

    const errorMessageId = id + '-errorMessage';
    const ariaActiveDescendant = disabled
      ? undefined
      : isOpen && selectedIndices.length === 1 && selectedIndices[0] >= 0
      ? this._listId + selectedIndices[0]
      : undefined;

    this._classNames = getClassNames(propStyles, {
      theme,
      className,
      hasError: !!(errorMessage && errorMessage.length > 0),
      hasLabel: !!label,
      isOpen,
      required,
      disabled,
      isRenderingPlaceholder: !selectedOptions.length,
      panelClassName: panelProps ? panelProps.className : undefined,
      calloutClassName: calloutProps ? calloutProps.className : undefined,
      calloutRenderEdge: calloutRenderEdge,
    });

    const hasErrorMessage: boolean = !!errorMessage && errorMessage.length > 0;

    return (
      <div
        className={this._classNames.root}
        ref={this.props.hoisted.rootRef}
        aria-owns={isOpen ? this._listId : undefined}
      >
        {onRenderLabel(this.props, this._onRenderLabel)}
        <div
          data-is-focusable={!disabled}
          data-ktp-target={true}
          ref={this._dropDown}
          id={id}
          tabIndex={disabled ? -1 : 0}


onRenderLabel(this


onRenderCaretDown(props, this


// If this is a controlled component then no state change should take place.
if (selectedKey


onRenderList(props, this


export class ParameterDto {
  code: number;
  name: string;
  state: boolean;

  constructor(values: Object = {}) {
    Object.assign(this, values);
  }
}


/**
 * Cancel token, entity class.
 */
export declare class Cancel extends Class.Null {
    /**
     * Card token.
     * Use 32 characters.
     */
    paymentMethodToken: string;
}


(obj): obj is {field: string; direction?: 'asc' | 'desc'} => {
            return (
                isPlainObject(obj) &&
                isString(obj.field) &&
                (obj.direction === void 0 || includes(['asc', 'desc'], obj.direction))
            );
        }


(cellFormat): cellFormat is 'json' | 'string' => {
        return isString(cellFormat) && includes(['json', 'string'], cellFormat);
    }


export type QueryParams = {
    [key in keyof typeof paramValidators]?: Parameters<typeof paramValidators[key]>[0];
};


class compareResults {
    public misMatchPercentage: number;
    public isWithinMisMatchTolerance: boolean;
    public isSameDimensions: boolean;
    public isExactSameImage: boolean;
  }


interface IntrinsicAttributes {
    store?: any;
  }


interface Assertion {
    matchSnapshot(resetSnapshot?: boolean): void;
  }


interface Client<T> {
    saveViewportScreenshot(filename?: string, options?: Object): void;
    saveElementScreenshot(filename: string | undefined, elementSelector: string, options?: Object): void;
    saveDocumentScreenshot(filename?: string, options?: Object): void;
    checkViewport(options?: Object): compareResults[];
    checkDocument(options?: Object): compareResults[];
    checkElement(elementSelector: string, options?: Object): compareResults[];
  }


type Element = React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & { class: string, children: any };


flush();


value => Number(value)


balance => {
        if (balance > 0) {
          const methods = Array.from(Array(balance)).map((_, index) => ({
            method: 'tokenOfOwnerByIndex',
            methodArgs: [this.account, index],
            transform: (value: any) => Number(value),
          }));

          return this.batch(methods);
        }

        return [];
      }


(_, index) => ({
            method: 'tokenOfOwnerByIndex',
            methodArgs: [this.account, index],
            transform: (value: any) => Number(value),
          })


(value: any) => Number(value)


class SYJuniorBondContract extends Web3Contract {
  constructor(address: string) {
    super(ABI, address, '');
  }

  async getJuniorBondIds(): Promise<number[]> {
    if (!this.account) {
      return Promise.reject();
    }

    return this.call('balanceOf', [this.account])
      .then(value => Number(value))
      .then(balance => {
        if (balance > 0) {
          const methods = Array.from(Array(balance)).map((_, index) => ({
            method: 'tokenOfOwnerByIndex',
            methodArgs: [this.account, index],
            transform: (value: any) => Number(value),
          }));

          return this.batch(methods);
        }

        return [];
      });
  }
}


async getJuniorBondIds(): Promise<number[]> {
    if (!this.account) {
      return Promise.reject();
    }

    return this.call('balanceOf', [this.account])
      .then(value => Number(value))
      .then(balance => {
        if (balance > 0) {
          const methods = Array.from(Array(balance)).map((_, index) => ({
            method: 'tokenOfOwnerByIndex',
            methodArgs: [this.account, index],
            transform: (value: any) => Number(value),
          }));

          return this.batch(methods);
        }

        return [];
      });
  }


export function register(id: string): boolean;


export class Client extends EventEmitter {
    constructor(options: RPCClientOptions)

    application: {
        description: string
        icon: string
        id: string
        rpc_origins: string[]
        name: string
    };

    user: {
        username: string
        discriminator: string
        id: string
        avatar: string
    };

    connect(clientId: string): Promise<Client>;
    login(options?: RPCLoginOptions): Promise<this>;

    getGuild(id: string, timeout?: number): Promise<Guild>;
    getGuilds(timeout?: number): Promise<Guild[]>;

    getChannel(id: string, timeout?: number): Promise<Channel>;
    getChannels(id?: string, timeout?: number): Promise<Channel[]>;

    setCertifiedDevices(devices: CertifiedDevice[]): Promise<null>;

    setUserVoiceSettings(id: string, settings: UserVoiceSettings): Promise<any>;

    selectVoiceChannel(id: string, options?: { timeout?: number | undefined, force?: boolean | undefined }): Promise<Channel>;
    selectTextChannel(id: string, options?: { timeout: number, force: boolean }): Promise<Channel>;

    getVoiceSettings(): Promise<VoiceSettings>;
    setVoiceSettings(args: VoiceSettings): Promise<any>;

    captureShortcut(callback: (shortcut: Array<{ type: number, code: number, name: string }>, stop: () => void) => void): Promise<() => void>;

    setActivity(args?: Presence, pid?: number): Promise<any>;
    clearActivity(pid?: number): Promise<any>;

    sendJoinInvite(user: { id: string } | string): Promise<any>;

    sendJoinRequest(user: { id: string } | string): Promise<any>;
    closeJoinRequest(user: { id: string } | string): Promise<any>;

    createLobby(type: string, capacity: number, metadata: any): Promise<any>;
    updateLobby(lobby: { id: string } | string, options?: { type: string, owner: { id: string } | string, capacity: number, metadata: any }): Promise<any>;
    deleteLobby(lobby: { id: string } | string): Promise<any>;
    connectToLobby(id: string, secret: string): Promise<any>;
    sendToLobby(lobby: { id: string } | string, data: any): Promise<any>;
    disconnectFromLobby(lobby: { id: string } | string): Promise<any>;
    updateLobbyMember(lobby: { id: string } | string, user: { id: string } | string, metadata: any): Promise<any>;

    subscribe(event: string, callback: (data: any) => void): Promise<Subscription>;
    subscribe(event: string, args: any, callback: (data: any) => void): Promise<Subscription>;

    destroy(): Promise<void>;

    on(event: 'ready' | 'connected', listener: () => void): this;
    once(event: 'ready' | 'connected', listener: () => void): this;
    off(event: 'ready' | 'connected', listener: () => void): this;
}


export interface RPCClientOptions {
    transport: 'ipc' | 'websocket';
}


export interface RPCLoginOptions {
    clientId: string;
    clientSecret?: string | undefined;
    accessToken?: string | undefined;
    rpcToken?: string | undefined;
    redirectUri?: string | undefined;
    scopes?: string[] | undefined;
    prompt?: 'none' | 'consent' | undefined;
}


export interface Guild {
    id: string;
    name: string;
    icon_url?: string | undefined;
    members?: any[] | undefined;
}


export interface Subscription {
    unsubscribe(): Promise<boolean>;
}


export interface Channel {
    id: string;
    name: string;

  /**
   * Guild text: 0, Guild voice: 2, DM: 1, Group DM: 3
   */
    type: number;

    guild_id?: string | undefined;

  /**
   * (text)
   */
    topic?: string | undefined;

  /**
   * (voice)
   */
    bitrate?: number | undefined;

  /**
   * (voice) 0 if none
   */
    user_limit?: number | undefined;

  /**
   * (text)
   */
    position?: number | undefined;

  /**
   * (voice) https://discordapp.com/developers/docs/resources/voice#voice-state-object
   */
    voice_states?: any[] | undefined;

  /**
   * (text) https://discordapp.com/developers/docs/resources/channel#message-object
   */
    messages?: any[] | undefined;
}


export interface CertifiedDevice {
    type: 'audioinput' | 'audiooutput' | 'videoinput';
    uuid: string;
    vendor: { name: string, url: string };
    model: { name: string, url: string };
    related: string[];
    echoCancellation?: boolean | undefined;
    noiseSuppression?: boolean | undefined;
    automaticGainControl?: boolean | undefined;
    hardwareMute?: boolean | undefined;
}


export interface UserVoiceSettings {
    id: string;
    pan?: { left: number, right: number } | undefined;
    volume?: number | undefined;
    mute?: boolean | undefined;
}


export interface VoiceSettings {
    automaticGainControl: boolean;
    echoCancellation: boolean;
    noiseSuppression: boolean;
    qos: boolean;
    silenceWarning: boolean;
    deaf: boolean;
    mute: boolean;
    input?: {
        device: string,
        volume: number
    } | undefined;
    output?: {
        device: string,
        volume: number
    } | undefined;
    mode?: {
        autoThreshold: boolean,
        threshold: number,
        shortcut: Array<{ type: number, code: number, name: string }>,
        delay: number
    } | undefined;
}


export interface Presence {
    state?: string | undefined;
    details?: string | undefined;
    startTimestamp?: number | Date | undefined;
    endTimestamp?: number | Date | undefined;
    largeImageKey?: string | undefined;
    largeImageText?: string | undefined;
    smallImageKey?: string | undefined;
    smallImageText?: string | undefined;
    instance?: boolean | undefined;
    partyId?: string | undefined;
    partySize?: number | undefined;
    partyMax?: number | undefined;
    matchSecret?: string | undefined;
    spectateSecret?: string | undefined;
    joinSecret?: string | undefined;
    buttons?: Array<{ label: string, url: string }> | undefined;
}


connect(clientId: string): Promise<Client>;


login(options?: RPCLoginOptions): Promise<this>;


getGuild(id: string, timeout?: number): Promise<Guild>;


getGuilds(timeout?: number): Promise<Guild[]>;


getChannel(id: string, timeout?: number): Promise<Channel>;


getChannels(id?: string, timeout?: number): Promise<Channel[]>;


setCertifiedDevices(devices: CertifiedDevice[]): Promise<null>;


setUserVoiceSettings(id: string, settings: UserVoiceSettings): Promise<any>;


selectVoiceChannel(id: string, options?: { timeout?: number | undefined, force?: boolean | undefined }): Promise<Channel>;


selectTextChannel(id: string, options?: { timeout: number, force: boolean }): Promise<Channel>;


getVoiceSettings(): Promise<VoiceSettings>;


setVoiceSettings(args: VoiceSettings): Promise<any>;


captureShortcut(callback: (shortcut: Array<{ type: number, code: number, name: string }>, stop: () => void) => void): Promise<() => void>;


setActivity(args?: Presence, pid?: number): Promise<any>;


clearActivity(pid?: number): Promise<any>;


sendJoinInvite(user: { id: string } | string): Promise<any>;


sendJoinRequest(user: { id: string } | string): Promise<any>;


closeJoinRequest(user: { id: string } | string): Promise<any>;


createLobby(type: string, capacity: number, metadata: any): Promise<any>;


updateLobby(lobby: { id: string } | string, options?: { type: string, owner: { id: string } | string, capacity: number, metadata: any }): Promise<any>;


deleteLobby(lobby: { id: string } | string): Promise<any>;


connectToLobby(id: string, secret: string): Promise<any>;


sendToLobby(lobby: { id: string } | string, data: any): Promise<any>;


disconnectFromLobby(lobby: { id: string } | string): Promise<any>;


updateLobbyMember(lobby: { id: string } | string, user: { id: string } | string, metadata: any): Promise<any>;


subscribe(event: string, callback: (data: any) => void): Promise<Subscription>;


subscribe(event: string, args: any, callback: (data: any) => void): Promise<Subscription>;


destroy(): Promise<void>;


on(event: 'ready' | 'connected', listener: () => void): this;


once(event: 'ready' | 'connected', listener: () => void): this;


off(event: 'ready' | 'connected', listener: () => void): this;


interface Option {
      /** 接口调用结束的回调函数（调用成功、失败都会执行） */
      complete?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用失败的回调函数 */
      fail?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用成功的回调函数 */
      success?: (res: TaroGeneral.CallbackResult) => void
    }


interface Option {
      /** 接口调用结束的回调函数（调用成功、失败都会执行） */
      complete?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用失败的回调函数 */
      fail?: (res: TaroGeneral.CallbackResult) => void
      /** 是否与其他音频混播，设置为 true 之后，不会终止其他应用或微信内的音乐 */
      mixWithOther?: boolean
      /** （仅在 iOS 生效）是否遵循静音开关，设置为 false 之后，即使是在静音模式下，也能播放声音 */
      obeyMuteSwitch?: boolean
      /** 接口调用成功的回调函数 */
      success?: (res: TaroGeneral.CallbackResult) => void
    }


interface Option {
      /** 需要播放的语音文件的文件路径 */
      filePath: string
      /** 接口调用结束的回调函数（调用成功、失败都会执行） */
      complete?: (res: TaroGeneral.CallbackResult) => void
      /** 指定录音时长，到达指定的录音时长后会自动停止录音，单位：秒 */
      duration?: number
      /** 接口调用失败的回调函数 */
      fail?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用成功的回调函数 */
      success?: (res: TaroGeneral.CallbackResult) => void
    }


interface Option {
      /** 接口调用结束的回调函数（调用成功、失败都会执行） */
      complete?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用失败的回调函数 */
      fail?: (res: TaroGeneral.CallbackResult) => void
      /** 接口调用成功的回调函数 */
      success?: (result: SuccessCallbackResult) => void
    }


interface SuccessCallbackResult extends TaroGeneral.CallbackResult {
      /** 支持的音频输入源列表，可在 [RecorderManager.start()](/docs/apis/media/recorder/RecorderManager#start)用。返回值定义参考 https://developer.android.com/reference/kotlin/android/media/MediaRecorder.AudioSource */
      audioSources: Array<keyof audioSources>
      /** 调用结果 */
      errMsg: string
    }


/** 支持的音频输入源 */
interface audioSources {
      /** 自动设置，默认使用手机麦克风，插上耳麦后自动切换使用耳机麦克风，所有平台适用 */
      'auto'
      /** 手机麦克风，仅限 iOS */
      'buildInMic'
      /** 耳机麦克风，仅限 iOS */
      'headsetMic'
      /** 麦克风（没插耳麦时是手机麦克风，插耳麦时是耳机麦克风），仅限 Android */
      'mic'
      /** 同 mic，适用于录制音视频内容，仅限 Android */
      'camcorder'
      /** 同 mic，适用于实时沟通，仅限 Android */
      'voice_communication'
      /** 同 mic，适用于语音识别，仅限 Android */
      'voice_recognition'
    }


/** AudioBuffer 接口表示存在内存里的一段短小的音频资源，利用 [WebAudioContext.decodeAudioData](./WebAudioContext#decodeaudiodata) 方法从一个音频文件构建，或者利用 [AudioContext.createBuffer](https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/(AudioContext.createBuffer).html) 从原始数据构建。把音频放入 AudioBuffer 后，可以传入到一个 AudioBufferSourceNode 进行播放。
   * @supported weapp
   * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioBuffer.html
   */
interface AudioBuffer {
    /** 存储在缓存区的PCM数据的采样率（单位为sample/s) */
    sampleRate: number

    /** 返回存储在缓存区的PCM数据的采样帧率 */
    length: number

    /** 返回存储在缓存区的PCM数据的时长（单位为秒） */
    duration: number

    /** 储存在缓存区的PCM数据的通道数 */
    numberOfChannels: number

    /** 返回一个 Float32Array，包含了带有频道的PCM数据，由频道参数定义（有0代表第一个频道）
     * @supported weapp
     * @example
     * ```tsx
     * const audioCtx = Taro.createWebAudioContext()
     * const myArrayBuffer = audioCtx.createBuffer(2, frameCount, audioCtx.sampleRate);
     * const nowBuffering = myArrayBuffer.getChannelData(channel);
     * ```
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioBuffer.getChannelData.html
     */
    getChannelData(channel: number): Float32Array

    /** 从 AudioBuffer 的指定频道复制到数组终端。
     * @supported weapp
     * @example
     * ```tsx
     * const audioCtx = Taro.createWebAudioContext()
     * const audioBuffer = audioCtx.createFromAudioFile({
     *   filePath:'/pages/res/bgm.mp3', // 静态资源
     *   mixToMono:true,
     *   sampleRate:44100
     * });
     * const channels = audioBuffer.numberOfChannels
     * const anotherArray = new Float32Array(frameCount);
     * const rate = audioBuffer.sampleRate
     * const startOffSet = 0
     * const endOffset = rate * 3;
     * const newAudioBuffer = audioCtx.createBuffer(channels,endOffset - startOffset,rate)
     * const offset = 0
     *
     * for (let channel = 0; channel < channels; channel++) {
     *   audioBuffer.copyFromChannel(anotherArray, channel, startOffset);
     *   newAudioBuffer.copyToChannel(anotherArray, channel, offset);
     * }
     * ```
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioBuffer.copyFromChannel.html
     */
    copyFromChannel(): void

    /** 从指定数组复制样本到 audioBuffer 的特定通道
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioBuffer.copyToChannel.html
     */
    copyToChannel(
      /** 需要复制的源数组 */
      source: Float32Array,
      /** 需要复制到的目的通道号 */
      channelNumber: number,
      /** 复制偏移数据量 */
      startInChannel: number
    ): void
  }


/** `AudioContext` 实例，可通过 `Taro.createAudioContext` 获取。
   *
   * `AudioContext` 通过 `id` 跟一个 `audio` 组件绑定，操作对应的 audio 组件。
   * @supported weapp
   * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.html
   */
interface AudioContext {
    /** 暂停音频。
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.pause.html
     */
    pause(): void
    /** 播放音频。
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.play.html
     */
    play(): void
    /** 跳转到指定位置。
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.seek.html
     */
    seek(
      /** 跳转位置，单位 s */
      position: number,
    ): void
    /** 设置音频地址
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/AudioContext.setSrc.html
     */
    setSrc(
      /** 音频地址 */
      src: string,
    ): void
  }


interface onErrorDetail extends TaroGeneral.CallbackResult {
      /** 错误码 */
      errCode: number
      /** 错误信息 */
      errMsg: string
    }


interface onErrorDetailErrCode {
      /** 系统错误 */
      10001
      /** 网络错误 */
      10002
      /** 文件错误 */
      10003
      /** 格式错误 */
      10004
      /** 未知错误 */
      '-1'
    }


/** MediaAudioPlayer 实例，可通过 [Taro.createMediaAudioPlayer](./createMediaAudioPlayer) 接口获取实例。
   * @supported weapp
   * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.html
   */
interface MediaAudioPlayer {
    /** 音量。范围 0~1
     * @default 1
     */
    volume: number

    /** 启动播放器
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.start.html
     */
    start(): Promise<void>

    /** 添加音频源
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.addAudioSource.html
     */
    addAudioSource(
      /** 视频解码器实例。作为音频源添加到音频播放器中 */
      source: VideoDecoder
    ): Promise<void>

    /** 移除音频源
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.removeAudioSource.html
     */
    removeAudioSource(
      /** 视频解码器实例 */
      source: VideoDecoder
    ): Promise<void>

    /** 停止播放器
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.stop.html
     */
    stop(): Promise<void>

    /** 销毁播放器
     * @supported weapp
     * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/MediaAudioPlayer.destroy.html
     */
    destroy(): Promise<void>
  }


/** 字典对象 */
interface Constraints {
        /** 如果指定为 true 则禁用标准化
         * @default false
         */
        disableNormalization?: boolean
      }


/** 一类音频处理模块，不同的Node具备不同的功能，如GainNode(音量调整)等。一个 WebAudioContextNode 可以通过上下文来创建。
   *
   * > 目前已经支持以下Node： IIRFilterNode WaveShaperNode ConstantSourceNode ChannelMergerNode OscillatorNode GainNode BiquadFilterNode PeriodicWaveNode BufferSourceNode ChannelSplitterNode ChannelMergerNode DelayNode DynamicsCompressorNode ScriptProcessorNode PannerNode
   * @supported weapp
   * @see https://developers.weixin.qq.com/miniprogram/dev/api/media/audio/WebAudioContextNode.html
   */
interface WebAudioContextNode {
    /** 右手笛卡尔坐标系中X轴的位置。 */
    positionX: number

    /** 右手笛卡尔坐标系中Y轴的位置。 */
    positionY: number

    /** 右手笛卡尔坐标系中Z轴的位置。 */
    positionZ: number

    /** 表示监听器的前向系统在同一笛卡尔坐标系中的水平位置，作为位置（位置x，位置和位置和位置）值。 */
    forwardX: number

    /** 表示听众的前向方向在同一笛卡尔坐标系中作为位置（位置x，位置和位置和位置）值的垂直位置。 */
    forwardY: number

    /** 表示与position (positionX、positionY和positionZ)值在同一笛卡尔坐标系下的听者前进方向的纵向(前后)位置。 */
    forwardZ: number

    /** 表示在与position (positionX、positionY和positionZ)值相同的笛卡尔坐标系中侦听器向前方向的水平位置。 */
    upX: number

    /** 表示在与position (positionX、positionY和positionZ)值相同的笛卡尔坐标系中侦听器向上方向的水平位置。 */
    upY: number

    /** 表示在与position (positionX、positionY和positionZ)值相同的笛卡尔坐标系中侦听器向后方向的水平位置。 */
    upZ: number

    /** 设置监听器的方向
     * @supported weapp
     */
    setOrientation(...args: any[]): void

    /** 设置监听器的位置
     * @supported weapp
     */
    setPosition(...args: any[]): void
  }


function assertEqual<
	T1 extends {
		keys(): IterableIterator<string>;
		has(key: string): boolean;
		get(key: string): any;
	},
	T2 extends {
		keys(): IterableIterator<string>;
		has(key: string): boolean;
		get(key: string): any;
	},
>(one: T1, two: T2) {
	for (const key of one.keys()) {
		expect(two.has(key)).toBeTrue();
		expect(two.get(key)).toBe(one.get(key));
	}
}


async function assertFileContent(content: string): Promise<void> {
			await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe(
				content,
			);
		}


async function assertCleanedUp() {
			// The other files should have been cleaned up
			await expect(fs.pathExists(testFilenameFull)).resolves.toBeTrue();
			await expect(
				fs.pathExists(testFilenameFull + ".bak"),
			).resolves.toBeFalse();
			await expect(
				fs.pathExists(testFilenameFull + ".dump"),
			).resolves.toBeFalse();
		}


() => {
	const originalFS = jest.requireActual("fs-extra");
	// eslint-disable-next-line @typescript-eslint/no-var-requires
	const wait = require("alcalzone-shared/async").wait;
	return {
		__esModule: true, // Use it when dealing with esModules
		...originalFS,
		appendFile: jest.fn().mockImplementation(async (...args) => {
			if (mockAppendFileThrottle > 0) {
				await wait(mockAppendFileThrottle);
			}
			return originalFS.appendFile(...args);
		}),
		move: jest.fn().mockImplementation(async (...args) => {
			if (mockMoveFileThrottle > 0) {
				await wait(mockMoveFileThrottle);
			}
			return originalFS.move(...args);
		}),
		createReadStream: jest.fn().mockImplementation((path, options) => {
			// eslint-disable-next-line @typescript-eslint/no-var-requires
			const { PassThrough } = require("stream");
			const { /*fd,*/ encoding } = options;
			// const file = fd
			// 	? originalFS.readFileSync(fd, encoding)
			// 	: originalFS.readFileSync(path, encoding);
			const file = originalFS.readFileSync(path, encoding);
			const ret = new PassThrough();
			ret.write(file, encoding, () => {
				ret.end();
			});
			return ret;
		}),
	};
}


async (...args) => {
			if (mockAppendFileThrottle > 0) {
				await wait(mockAppendFileThrottle);
			}
			return originalFS.appendFile(...args);
		}


async (...args) => {
			if (mockMoveFileThrottle > 0) {
				await wait(mockMoveFileThrottle);
			}
			return originalFS.move(...args);
		}


(path, options) => {
			// eslint-disable-next-line @typescript-eslint/no-var-requires
			const { PassThrough } = require("stream");
			const { /*fd,*/ encoding } = options;
			// const file = fd
			// 	? originalFS.readFileSync(fd, encoding)
			// 	: originalFS.readFileSync(path, encoding);
			const file = originalFS.readFileSync(path, encoding);
			const ret = new PassThrough();
			ret.write(file, encoding, () => {
				ret.end();
			});
			return ret;
		}


() => {
				ret.end();
			}


() => {
		describe("validates autoCompress options", () => {
			it("sizeFactor <= 1", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactor: 0.9,
							},
						}),
				).toThrowError("sizeFactor");
			});

			it("minimumSize <= 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactorMinimumSize: -1,
							},
						}),
				).toThrowError("sizeFactorMinimumSize");
			});

			it("intervalMs < 10", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMs: 9,
							},
						}),
				).toThrowError("intervalMs");
			});

			it("intervalMinChanges < 10", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMinChanges: 0,
							},
						}),
				).toThrowError("intervalMinChanges");
			});
		});
		describe("validates throttleFS options", () => {
			it("intervalMs < 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: -1,
							},
						}),
				).toThrowError("intervalMs");
			});

			it("maxBufferedCommands < 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: 0,
								maxBufferedCommands: -1,
							},
						}),
				).toThrowError("maxBufferedCommands");
			});
		});
	}


() => {
			it("sizeFactor <= 1", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactor: 0.9,
							},
						}),
				).toThrowError("sizeFactor");
			});

			it("minimumSize <= 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactorMinimumSize: -1,
							},
						}),
				).toThrowError("sizeFactorMinimumSize");
			});

			it("intervalMs < 10", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMs: 9,
							},
						}),
				).toThrowError("intervalMs");
			});

			it("intervalMinChanges < 10", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMinChanges: 0,
							},
						}),
				).toThrowError("intervalMinChanges");
			});
		}


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactor: 0.9,
							},
						}),
				).toThrowError("sizeFactor");
			}


() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactor: 0.9,
							},
						})


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactorMinimumSize: -1,
							},
						}),
				).toThrowError("sizeFactorMinimumSize");
			}


() =>
						new JsonlDB("foo", {
							autoCompress: {
								sizeFactorMinimumSize: -1,
							},
						})


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMs: 9,
							},
						}),
				).toThrowError("intervalMs");
			}


() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMs: 9,
							},
						})


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMinChanges: 0,
							},
						}),
				).toThrowError("intervalMinChanges");
			}


() =>
						new JsonlDB("foo", {
							autoCompress: {
								intervalMinChanges: 0,
							},
						})


() => {
			it("intervalMs < 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: -1,
							},
						}),
				).toThrowError("intervalMs");
			});

			it("maxBufferedCommands < 0", () => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: 0,
								maxBufferedCommands: -1,
							},
						}),
				).toThrowError("maxBufferedCommands");
			});
		}


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: -1,
							},
						}),
				).toThrowError("intervalMs");
			}


() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: -1,
							},
						})


() => {
				expect(
					() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: 0,
								maxBufferedCommands: -1,
							},
						}),
				).toThrowError("maxBufferedCommands");
			}


() =>
						new JsonlDB("foo", {
							throttleFS: {
								intervalMs: 0,
								maxBufferedCommands: -1,
							},
						})


() => {
		let testFS: TestFS;
		let testFSRoot: string;
		beforeEach(async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();

			await testFS.create({
				yes:
					// Final newline omitted on purpose
					'{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key1"}',
				emptyLines:
					'\n{"k":"key1","v":1}\n\n\n{"k":"key2","v":"2"}\n\n',
				broken: `{"k":"key1","v":1}\n{"k":,"v":1}\n`,
				broken2: `{"k":"key1","v":1}\n{"k":"key2","v":}\n`,
				broken3: `{"k":"key1"\n`,
				reviver: `
{"k":"key1","v":1}
{"k":"key2","v":"2"}
{"k":"key1"}
{"k":"key1","v":true}`,
			});
		});
		afterEach(async () => {
			await testFS.remove();
		});

		it("sets the isOpen property to true", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();
			expect(db.isOpen).toBeTrue();
			await db.close();
		});

		it("checks if the given file exists and creates it if it doesn't", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "no"));
			await db.open();
			await db.close();
		});

		it("also creates leading directories if they don't exist", async () => {
			const db = new JsonlDB(
				path.join(testFSRoot, "this/path/does/not/exist"),
			);
			await db.open();
			await db.close();
		});

		it("also creates leading directories for the lockfiles if they don't exist", async () => {
			const lockfileDirectory = path.join(
				testFSRoot,
				"this/path/does/not/exist/either",
			);
			const db = new JsonlDB(path.join(testFSRoot, "lockfile"), {
				lockfileDirectory,
			});
			await db.open();
			await db.close();

			await expect(fs.pathExists(lockfileDirectory)).resolves.toBeTrue();
		});

		it("reads the file if it exists", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();
			await db.close();
		});

		it("throws if another DB has opened the DB file at the same time", async () => {
			const db1 = new JsonlDB(path.join(testFSRoot, "yes"));
			await db1.open();

			const db2 = new JsonlDB(path.join(testFSRoot, "yes"));
			try {
				await db2.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/Failed to lock/i);
			}

			await db1.close();

			await db2.open();
			await db2.close();
		});

		it("should contain the correct data", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();

			expect(db.size).toBe(1);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeTrue();
			expect(db.get("key2")).toBe("2");

			const spy = jest.fn();
			db.forEach(spy);
			expect(spy).toBeCalledTimes(1);
			expect(spy.mock.calls[0].slice(0, 2)).toEqual(["2", "key2"]);

			await db.close();
		});

		it("skips empty input lines", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "emptyLines"));
			await db.open();

			expect(db.has("key1")).toBeTrue();
			expect(db.get("key1")).toBe(1);
			expect(db.has("key2")).toBeTrue();
			expect(db.get("key2")).toBe("2");

			await db.close();
		});

		it("throws when the file contains invalid JSON", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 2");
			}
		});

		it("throws when the file contains invalid JSON (part 2)", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken2"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 2");
			}
		});

		it("throws when the file contains invalid JSON (part 3)", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken3"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 1");
			}
		});

		it("does not throw when the file contains invalid JSON and `ignoreReadErrors` is true", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken"), {
				ignoreReadErrors: true,
			});
			await expect(db.open()).toResolve();
			await db.close();
		});

		it("does not throw when the file contains invalid JSON and `ignoreReadErrors` is true (part 2)", async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken2"), {
				ignoreReadErrors: true,
			});
			await expect(db.open()).toResolve();
			await db.close();
		});

		it("transforms each value using the valueReviver function if any is passed", async () => {
			const reviver = jest.fn().mockReturnValue("eeee");
			const db = new JsonlDB(path.join(testFSRoot, "reviver"), {
				reviver,
			});
			await db.open();
			expect(reviver).toBeCalledTimes(2);
			expect(reviver).toBeCalledWith("key2", "2");
			expect(reviver).toBeCalledWith("key1", true);

			db.forEach((v) => {
				expect(v).toBe("eeee");
			});
			await db.close();
		});
	}


async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();

			await testFS.create({
				yes:
					// Final newline omitted on purpose
					'{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key1"}',
				emptyLines:
					'\n{"k":"key1","v":1}\n\n\n{"k":"key2","v":"2"}\n\n',
				broken: `{"k":"key1","v":1}\n{"k":,"v":1}\n`,
				broken2: `{"k":"key1","v":1}\n{"k":"key2","v":}\n`,
				broken3: `{"k":"key1"\n`,
				reviver: `
{"k":"key1","v":1}
{"k":"key2","v":"2"}
{"k":"key1"}
{"k":"key1","v":true}`,
			});
		}


async () => {
			await testFS.remove();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();
			expect(db.isOpen).toBeTrue();
			await db.close();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "no"));
			await db.open();
			await db.close();
		}


async () => {
			const db = new JsonlDB(
				path.join(testFSRoot, "this/path/does/not/exist"),
			);
			await db.open();
			await db.close();
		}


async () => {
			const lockfileDirectory = path.join(
				testFSRoot,
				"this/path/does/not/exist/either",
			);
			const db = new JsonlDB(path.join(testFSRoot, "lockfile"), {
				lockfileDirectory,
			});
			await db.open();
			await db.close();

			await expect(fs.pathExists(lockfileDirectory)).resolves.toBeTrue();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();
			await db.close();
		}


async () => {
			const db1 = new JsonlDB(path.join(testFSRoot, "yes"));
			await db1.open();

			const db2 = new JsonlDB(path.join(testFSRoot, "yes"));
			try {
				await db2.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/Failed to lock/i);
			}

			await db1.close();

			await db2.open();
			await db2.close();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "yes"));
			await db.open();

			expect(db.size).toBe(1);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeTrue();
			expect(db.get("key2")).toBe("2");

			const spy = jest.fn();
			db.forEach(spy);
			expect(spy).toBeCalledTimes(1);
			expect(spy.mock.calls[0].slice(0, 2)).toEqual(["2", "key2"]);

			await db.close();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "emptyLines"));
			await db.open();

			expect(db.has("key1")).toBeTrue();
			expect(db.get("key1")).toBe(1);
			expect(db.has("key2")).toBeTrue();
			expect(db.get("key2")).toBe("2");

			await db.close();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 2");
			}
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken2"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 2");
			}
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken3"));
			try {
				await db.open();
				throw new Error("it did not throw");
			} catch (e: any) {
				expect(e.message).toMatch(/invalid data/i);
				expect(e.message).toMatch("line 1");
			}
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken"), {
				ignoreReadErrors: true,
			});
			await expect(db.open()).toResolve();
			await db.close();
		}


async () => {
			const db = new JsonlDB(path.join(testFSRoot, "broken2"), {
				ignoreReadErrors: true,
			});
			await expect(db.open()).toResolve();
			await db.close();
		}


async () => {
			const reviver = jest.fn().mockReturnValue("eeee");
			const db = new JsonlDB(path.join(testFSRoot, "reviver"), {
				reviver,
			});
			await db.open();
			expect(reviver).toBeCalledTimes(2);
			expect(reviver).toBeCalledWith("key2", "2");
			expect(reviver).toBeCalledWith("key1", true);

			db.forEach((v) => {
				expect(v).toBe("eeee");
			});
			await db.close();
		}


(v) => {
				expect(v).toBe("eeee");
			}


() => {
		const testFilename = "clear.jsonl";
		let testFilenameFull: string;
		let db: JsonlDB;
		let testFS: TestFS;
		let testFSRoot: string;

		beforeEach(async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();
			testFilenameFull = path.join(testFSRoot, testFilename);
			await testFS.create({
				[testFilename]:
					'{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key1"}\n',
			});
			db = new JsonlDB(testFilenameFull);
			await db.open();
		});
		afterEach(async () => {
			await testFS.remove();
		});

		it("throws when the DB is not open", async () => {
			await db.close();
			expect(() => db.clear()).toThrowError("not open");
		});

		it("removes all entries from the database and truncates the file", async () => {
			db.clear();
			expect(db.size).toBe(0);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(fs.stat(testFilenameFull)).resolves.toMatchObject({
				size: 0,
			});
		});
	}


async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();
			testFilenameFull = path.join(testFSRoot, testFilename);
			await testFS.create({
				[testFilename]:
					'{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key1"}\n',
			});
			db = new JsonlDB(testFilenameFull);
			await db.open();
		}


async () => {
			await db.close();
			expect(() => db.clear()).toThrowError("not open");
		}


() => db.clear()


async () => {
			db.clear();
			expect(db.size).toBe(0);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(fs.stat(testFilenameFull)).resolves.toMatchObject({
				size: 0,
			});
		}


() => {
		const testFilename = "delete.jsonl";
		let testFilenameFull: string;
		let db: JsonlDB;
		let testFS: TestFS;
		let testFSRoot: string;

		beforeEach(async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();
			testFilenameFull = path.join(testFSRoot, testFilename);
			await testFS.create({
				[testFilename]: '{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n',
			});
			db = new JsonlDB(testFilenameFull);
			await db.open();
		});
		afterEach(async () => {
			await testFS.remove();
		});

		it("throws when the DB is not open", async () => {
			await db.close();
			expect(() => db.delete("key1")).toThrowError("not open");
		});

		it("removes the given key from the database and writes a line with an undefined value", async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.size).toBe(1);
			expect(db.has("key1")).toBeTrue();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(
				fs.readFile(testFilenameFull, "utf8"),
			).resolves.toEndWith(`{"k":"key2"}\n`);
		});

		it("removes multiple key from the database in the correct order", async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.delete("key1")).toBeTrue();
			expect(db.size).toBe(0);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(
				fs.readFile(testFilenameFull, "utf8"),
			).resolves.toEndWith(`{"k":"key2"}\n{"k":"key1"}\n`);
		});

		it("deleting a key twice does not write to the log twice", async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.delete("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe(
				`{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key2"}\n`,
			);
		});
	}


async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();
			testFilenameFull = path.join(testFSRoot, testFilename);
			await testFS.create({
				[testFilename]: '{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n',
			});
			db = new JsonlDB(testFilenameFull);
			await db.open();
		}


async () => {
			await db.close();
			expect(() => db.delete("key1")).toThrowError("not open");
		}


() => db.delete("key1")


async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.size).toBe(1);
			expect(db.has("key1")).toBeTrue();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(
				fs.readFile(testFilenameFull, "utf8"),
			).resolves.toEndWith(`{"k":"key2"}\n`);
		}


async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.delete("key1")).toBeTrue();
			expect(db.size).toBe(0);
			expect(db.has("key1")).toBeFalse();
			expect(db.has("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(
				fs.readFile(testFilenameFull, "utf8"),
			).resolves.toEndWith(`{"k":"key2"}\n{"k":"key1"}\n`);
		}


async () => {
			expect(db.delete("key2")).toBeTrue();
			expect(db.delete("key2")).toBeFalse();

			// Force the stream to be flushed
			await db.close();

			await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe(
				`{"k":"key1","v":1}\n{"k":"key2","v":"2"}\n{"k":"key2"}\n`,
			);
		}


() => {
		const testFilename = "set.jsonl";
		let testFilenameFull: string;
		let db: JsonlDB;
		let testFS: TestFS;
		let testFSRoot: string;

		beforeEach(async () => {
			testFS = new TestFS();
			testFSRoot = await testFS.getRoot();
			testFilenameFull = path.join(testFSRoot, testFilename);
			await testFS.create();
			db = new JsonlDB(testFilenameFull);
			await db.open();
		});

		afterEach(async () => {
			await testFS.remove();
		});

		it("throws when the DB is not open", async () => {
			await db.close();
			expect(() => db.set("foo", 1)).toThrowError("not open");
		});

		it("adds the given key to the database and writes a line with the serialized value", async () => {
			db.set("key", true);
			expect(db.size).toBe(1);
			expect(db.has("key")).toBeTrue();

			// Force the stream to be flushed
			await db.close();

			await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe(
				`{"k":"key","v":true}\n`,
			);
		});

		it("adds multiple keys to the database in the correct order", async () => {
			db.set("key2", true);
			db.set("key1", 1000);
			db.set("key3", "");

			// Force the stream to be flushed
			await db.close();

			await expect(fs.readFile(testFilenameFull, "utf8")).resolves.toBe(
				`{"k":"key2","v":true}\n{"k":"key1","v":1000}\n{"k":"key3","v":""}\n`,
			);
		});
	}