/**
 * @license
 * Copyright 2019 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  createGenUidStubWithReturns,
  sharedTestSetup,
  sharedTestTeardown,
  workspaceTeardown,
} from './test_helpers/setup_teardown.js';
import {
  defineRowBlock,
  defineStatementBlock,
  defineStackBlock,
} from './test_helpers/block_definitions.js';

suite('Connection', function () {
  setup(function () {
    sharedTestSetup.call(this);
    this.workspace = sinon.createStubInstance(Blockly.Workspace);
    this.workspace.connectionChecker = new Blockly.ConnectionChecker();
    this.createConnection = function (type) {
      const block = {
        workspace: this.workspace,
        isShadow: function () {
          return false;
        },
      };
      const connection = new Blockly.Connection(block, type);
      return connection;
    };
  });

  teardown(function () {
    sharedTestTeardown.call(this);
  });

  suite('Set Shadow', function () {
    function assertBlockMatches(block, isShadow, opt_id) {
      chai.assert.equal(
        block.isShadow(),
        isShadow,
        `expected block ${block.id} to ${isShadow ? '' : 'not'} be a shadow`,
      );
      if (opt_id) {
        chai.assert.equal(block.id, opt_id);
      }
    }

    function assertInputHasBlock(parent, inputName, isShadow, opt_name) {
      const block = parent.getInputTargetBlock(inputName);
      chai.assert.exists(
        block,
        `expected block ${opt_name || ''} to be attached to ${inputName}`,
      );
      assertBlockMatches(block, isShadow, opt_name);
    }

    function assertNextHasBlock(parent, isShadow, opt_name) {
      const block = parent.getNextBlock();
      chai.assert.exists(
        block,
        `expected block ${opt_name || ''} to be attached to next connection`,
      );
      assertBlockMatches(block, isShadow, opt_name);
    }

    function assertInputNotHasBlock(parent, inputName) {
      const block = parent.getInputTargetBlock(inputName);
      chai.assert.notExists(
        block,
        `expected block ${
          block && block.id
        } to not be attached to ${inputName}`,
      );
    }

    function assertNextNotHasBlock(parent) {
      const block = parent.getNextBlock();
      chai.assert.notExists(
        block,
        `expected block ${
          block && block.id
        } to not be attached to next connection`,
      );
    }

    function assertSerialization(block, jso, xmlText) {
      const actualJso = Blockly.serialization.blocks.save(block, {
        addNextBlocks: true,
      });
      const actualXml = Blockly.Xml.domToText(Blockly.Xml.blockToDom(block));
      chai.assert.deepEqual(actualJso, jso);
      chai.assert.equal(actualXml, xmlText);
    }

    const testSuites = [
      {
        title: 'Rendered',
        createWorkspace: () => {
          return Blockly.inject('blocklyDiv');
        },
      },
      {
        title: 'Headless',
        createWorkspace: () => {
          return new Blockly.Workspace();
        },
      },
    ];

    testSuites.forEach((testSuite) => {
      // Create a suite for each suite.
      suite(testSuite.title, function () {
        setup(function () {
          this.workspace = testSuite.createWorkspace();

          defineRowBlock();
          defineStatementBlock();
          defineStackBlock();

          createGenUidStubWithReturns(
            new Array(30).fill().map((_, i) => 'id' + i),
          );
        });

        teardown(function () {
          workspaceTeardown.call(this, this.workspace);
        });

        suite('setShadowDom', function () {
          suite('Add - No Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="row_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            function createStatementBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="statement_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            function createStackBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="stack_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="id1"/>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', true);
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="id1">' +
                  '  <value name="INPUT">' +
                  '    <shadow type="row_block" id="id2"/>' +
                  '  </value>' +
                  '</shadow>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id2"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="id1"/>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', true);
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="id1">' +
                  '  <statement name="NAME">' +
                  '    <shadow type="statement_block" id="id2"/>' +
                  '  </statement>' +
                  '</shadow>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id2"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="id1"/>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });

            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="id1">' +
                  '  <next>' +
                  '    <shadow type="stack_block" id="id2"/>' +
                  '  </next>' +
                  '</shadow>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'id2',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id2"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Add - With Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlocks(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="row_block" id="id0">' +
                    '  <value name="INPUT">' +
                    '    <block type="row_block" id="idA"/>' +
                    '  </value>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStatementBlocks(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="statement_block" id="id0">' +
                    '  <statement name="NAME">' +
                    '    <block type="statement_block" id="idA"/>' +
                    '  </statement>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStackBlocks(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="stack_block" id="id0">' +
                    '  <next>' +
                    '    <block type="stack_block" id="idA"/>' +
                    '  </next>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="id1"/>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="id1">' +
                  '  <value name="INPUT">' +
                  '    <shadow type="row_block" id="id2"/>' +
                  '  </value>' +
                  '</shadow>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', false);
              assertInputNotHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
              );
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id2"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="id1"/>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="id1">' +
                  '  <statement name="NAME">' +
                  '    <shadow type="statement_block" id="id2"/>' +
                  '  </statement>' +
                  '</shadow>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', false);
              assertInputNotHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
              );
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id2"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="id1"/>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });

            test('Multiple Next', function () {
              const parent = createStackBlocks(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="id1">' +
                  '  <next>' +
                  '    <shadow type="stack_block" id="id2"/>' +
                  '  </next>' +
                  '</shadow>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, false);
              assertNextNotHasBlock(parent.getNextBlock());
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'id2',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id2"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Add - With Shadow Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="row_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            function createStatementBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="statement_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            function createStackBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="stack_block" id="id0"/>',
                ),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="1"/>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml1);
              assertInputHasBlock(parent, 'INPUT', true, '1');
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="2"/>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml2);
              assertInputHasBlock(parent, 'INPUT', true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': '2',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="2"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="1">' +
                  '  <value name="INPUT">' +
                  '    <shadow type="row_block" id="a"/>' +
                  '  </value>' +
                  '</shadow>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml1);
              assertInputHasBlock(parent, 'INPUT', true, '1');
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
                'a',
              );
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="row_block" id="2">' +
                  '  <value name="INPUT">' +
                  '    <shadow type="row_block" id="b"/>' +
                  '  </value>' +
                  '</shadow>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml2);
              assertInputHasBlock(parent, 'INPUT', true, '2');
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
                'b',
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': '2',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'b',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="2">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="b"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="1"/>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml1);
              assertInputHasBlock(parent, 'NAME', true, '1');
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="2"/>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml2);
              assertInputHasBlock(parent, 'NAME', true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': '2',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="2"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="1">' +
                  '  <statement name="NAME">' +
                  '    <shadow type="statement_block" id="a"/>' +
                  '  </statement>' +
                  '</shadow>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml1);
              assertInputHasBlock(parent, 'NAME', true, '1');
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
                'a',
              );
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block" id="2">' +
                  '  <statement name="NAME">' +
                  '    <shadow type="statement_block" id="b"/>' +
                  '  </statement>' +
                  '</shadow>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml2);
              assertInputHasBlock(parent, 'NAME', true, '2');
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
                'b',
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': '2',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'b',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="2">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="b"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="1"/>',
              );
              parent.nextConnection.setShadowDom(xml1);
              assertNextHasBlock(parent, true, '1');
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="2"/>',
              );
              parent.nextConnection.setShadowDom(xml2);
              assertNextHasBlock(parent, true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': '2',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="2"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });

            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml1 = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="1">' +
                  '  <next>' +
                  '    <shadow type="stack_block" id="a"/>' +
                  '  </next>' +
                  '</shadow>',
              );
              parent.nextConnection.setShadowDom(xml1);
              assertNextHasBlock(parent, true, '1');
              assertNextHasBlock(parent.getNextBlock(), true, 'a');
              const xml2 = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="2">' +
                  '  <next>' +
                  '    <shadow type="stack_block" id="b"/>' +
                  '  </next>' +
                  '</shadow>',
              );
              parent.nextConnection.setShadowDom(xml2);
              assertNextHasBlock(parent, true, '2');
              assertNextHasBlock(parent.getNextBlock(), true, 'b');
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': '2',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'b',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="2">' +
                  '<next>' +
                  '<shadow type="stack_block" id="b"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Remove - No Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="row_block" id="id0">' +
                    '  <value name="INPUT">' +
                    '    <shadow type="row_block" id="idA"/>' +
                    '  </value>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStatementBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="statement_block" id="id0">' +
                    '  <statement name="NAME">' +
                    '    <shadow type="statement_block" id="idA"/>' +
                    '  </statement>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStackBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="stack_block" id="id0">' +
                    '  <next>' +
                    '    <shadow type="stack_block" id="idA"/>' +
                    '  </next>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              parent.getInput('INPUT').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'INPUT');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'STATEMENT');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowDom(null);
              assertNextNotHasBlock(parent);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '</block>',
              );
            });
          });

          suite('Remove - Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="row_block" id="id0">' +
                    '  <value name="INPUT">' +
                    '    <shadow type="row_block" id="idA"/>' +
                    '    <block type="row_block" id="idB"/>' +
                    '  </value>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStatementBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="statement_block" id="id0">' +
                    '  <statement name="NAME">' +
                    '    <shadow type="statement_block" id="idA"/>' +
                    '    <block type="statement_block" id="idB"/>' +
                    '  </statement>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            function createStackBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom(
                  '<block type="stack_block" id="id0">' +
                    '  <next>' +
                    '    <shadow type="stack_block" id="idA"/>' +
                    '    <block type="stack_block" id="idB"/>' +
                    '  </next>' +
                    '</block>',
                ),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              parent.getInput('INPUT').connection.setShadowDom(null);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputNotHasBlock(parent, 'INPUT');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowDom(null);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputNotHasBlock(parent, 'NAME');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowDom(null);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextNotHasBlock(parent);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '</block>',
              );
            });
          });

          suite('Add - Connect & Disconnect - Remove', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom('<block type="row_block"/>'),
                workspace,
              );
              return block;
            }

            function createStatementBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom('<block type="statement_block"/>'),
                workspace,
              );
              return block;
            }

            function createStackBlock(workspace) {
              const block = Blockly.Xml.domToBlock(
                Blockly.utils.xml.textToDom('<block type="stack_block"/>'),
                workspace,
              );
              return block;
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block"/>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', true);
              const child = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.connect(child.outputConnection);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              parent.getInput('INPUT').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'INPUT');
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="row_block">' +
                  '  <value name="INPUT">' +
                  '    <shadow type="row_block"/>' +
                  '  </value>' +
                  '</shadow>',
              );
              parent.getInput('INPUT').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              const child = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.connect(child.outputConnection);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              parent.getInput('INPUT').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'INPUT');
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block"/>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', true);
              const child = createStatementBlock(this.workspace);
              parent
                .getInput('NAME')
                .connection.connect(child.previousConnection);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              parent.getInput('NAME').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'NAME');
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="statement_block">' +
                  '  <statement name="NAME">' +
                  '    <shadow type="statement_block"/>' +
                  '  </statement>' +
                  '</shadow>',
              );
              parent.getInput('NAME').connection.setShadowDom(xml);
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              const child = createStatementBlock(this.workspace);
              parent
                .getInput('NAME')
                .connection.connect(child.previousConnection);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              parent.getInput('NAME').connection.setShadowDom(null);
              assertInputNotHasBlock(parent, 'NAME');
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block"/>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, true);
              const child = createStatementBlock(this.workspace);
              parent.nextConnection.connect(child.previousConnection);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              parent.nextConnection.setShadowDom(null);
              assertNextNotHasBlock(parent);
            });

            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              const xml = Blockly.utils.xml.textToDom(
                '<shadow type="stack_block" id="parent">' +
                  '  <next>' +
                  '    <shadow type="stack_block" id="child"/>' +
                  '  </next>' +
                  '</shadow>',
              );
              parent.nextConnection.setShadowDom(xml);
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              const child = createStatementBlock(this.workspace);
              parent.nextConnection.connect(child.previousConnection);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              parent.nextConnection.setShadowDom(null);
              assertNextNotHasBlock(parent);
            });
          });

          suite('Invalid', function () {
            test('Attach to output', function () {
              const block = this.workspace.newBlock('row_block');
              chai.assert.throws(() =>
                block.outputConnection.setShadowDom(
                  Blockly.utils.xml.textToDom('<block type="row_block">'),
                ),
              );
            });

            test('Attach to previous', function () {
              const block = this.workspace.newBlock('stack_block');
              chai.assert.throws(() =>
                block.previousConnection.setShadowDom(
                  Blockly.utils.xml.textToDom('<block type="stack_block">'),
                ),
              );
            });

            test('Missing output', function () {
              const block = this.workspace.newBlock('row_block');
              chai.assert.throws(() =>
                block.outputConnection.setShadowDom(
                  Blockly.utils.xml.textToDom('<block type="stack_block">'),
                ),
              );
            });

            test('Missing previous', function () {
              const block = this.workspace.newBlock('stack_block');
              chai.assert.throws(() =>
                block.previousConnection.setShadowDom(
                  Blockly.utils.xml.textToDom('<block type="row_block">'),
                ),
              );
            });

            test('Invalid connection checks, output', function () {
              const block = this.workspace.newBlock('logic_operation');
              chai.assert.throws(() =>
                block
                  .getInput('A')
                  .connection.setShadowDom(
                    Blockly.utils.xml.textToDom('<block type="stack_block">'),
                  ),
              );
            });

            test('Invalid connection checks, previous', function () {
              Blockly.defineBlocksWithJsonArray([
                {
                  'type': 'stack_checks_block',
                  'message0': '',
                  'previousStatement': 'check 1',
                  'nextStatement': 'check 2',
                },
              ]);
              const block = this.workspace.newBlock('stack_checks_block');
              chai.assert.throws(() =>
                block.nextConnection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<block type="stack_checks_block">',
                  ),
                ),
              );
            });
          });
        });

        suite('setShadowState', function () {
          suite('Add - No Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'row_block', 'id': 'id0'},
                workspace,
              );
            }

            function createStatementBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'statement_block', 'id': 'id0'},
                workspace,
              );
            }

            function createStackBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'stack_block', 'id': 'id0'},
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.setShadowState({'type': 'row_block', 'id': 'id1'});
              assertInputHasBlock(parent, 'INPUT', true);
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              parent.getInput('INPUT').connection.setShadowState({
                'type': 'row_block',
                'id': 'id1',
                'inputs': {
                  'INPUT': {
                    'shadow': {
                      'type': 'row_block',
                      'id': 'id2',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id2"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': 'id1',
              });
              assertInputHasBlock(parent, 'NAME', true);
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statment', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': 'id1',
                'inputs': {
                  'NAME': {
                    'shadow': {
                      'type': 'statement_block',
                      'id': 'id2',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id2"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': 'id1',
              });
              assertNextHasBlock(parent, true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': 'id1',
                'next': {
                  'shadow': {
                    'type': 'stack_block',
                    'id': 'id2',
                  },
                },
              });
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'id2',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id2"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Add - With Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'block': {
                        'type': 'row_block',
                        'id': 'idA',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStatementBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'block': {
                        'type': 'statement_block',
                        'id': 'idA',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStackBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'block': {
                      'type': 'stack_block',
                      'id': 'idA',
                    },
                  },
                },
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlocks(this.workspace);
              parent
                .getInput('INPUT')
                .connection.setShadowState({'type': 'row_block', 'id': 'id1'});
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlocks(this.workspace);
              parent.getInput('INPUT').connection.setShadowState({
                'type': 'row_block',
                'id': 'id1',
                'inputs': {
                  'INPUT': {
                    'shadow': {
                      'type': 'row_block',
                      'id': 'id2',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'INPUT', false);
              assertInputNotHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
              );
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id1">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="id2"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': 'id1',
              });
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': 'id1',
                'inputs': {
                  'NAME': {
                    'shadow': {
                      'type': 'statement_block',
                      'id': 'id2',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'NAME', false);
              assertInputNotHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
              );
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'id2',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id1">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="id2"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlocks(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': 'id1',
              });
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });

            test('Multiple Next', function () {
              const parent = createStackBlocks(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': 'id1',
                'next': {
                  'shadow': {
                    'type': 'stack_block',
                    'id': 'id2',
                  },
                },
              });
              assertNextHasBlock(parent, false);
              assertNextNotHasBlock(parent.getNextBlock());
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'id2',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id1">' +
                  '<next>' +
                  '<shadow type="stack_block" id="id2"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Add - With Shadow Connected', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'row_block', 'id': 'id0'},
                workspace,
              );
            }

            function createStatementBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'statement_block', 'id': 'id0'},
                workspace,
              );
            }

            function createStackBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'stack_block', 'id': 'id0'},
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.setShadowState({'type': 'row_block', 'id': '1'});
              assertInputHasBlock(parent, 'INPUT', true, '1');
              parent
                .getInput('INPUT')
                .connection.setShadowState({'type': 'row_block', 'id': '2'});
              assertInputHasBlock(parent, 'INPUT', true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': '2',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="2"></shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              parent.getInput('INPUT').connection.setShadowState({
                'type': 'row_block',
                'id': '1',
                'inputs': {
                  'INPUT': {
                    'shadow': {
                      'type': 'row_block',
                      'id': 'a',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'INPUT', true, '1');
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
                'a',
              );
              parent.getInput('INPUT').connection.setShadowState({
                'type': 'row_block',
                'id': '2',
                'inputs': {
                  'INPUT': {
                    'shadow': {
                      'type': 'row_block',
                      'id': 'b',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'INPUT', true, '2');
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
                'b',
              );
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': '2',
                        'inputs': {
                          'INPUT': {
                            'shadow': {
                              'type': 'row_block',
                              'id': 'b',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="2">' +
                  '<value name="INPUT">' +
                  '<shadow type="row_block" id="b"></shadow>' +
                  '</value>' +
                  '</shadow>' +
                  '</value>' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': '1',
              });
              assertInputHasBlock(parent, 'NAME', true, '1');
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': '2',
              });
              assertInputHasBlock(parent, 'NAME', true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': '2',
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="2"></shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': '1',
                'inputs': {
                  'NAME': {
                    'shadow': {
                      'type': 'statement_block',
                      'id': 'a',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'NAME', true, '1');
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
                'a',
              );
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'id': '2',
                'inputs': {
                  'NAME': {
                    'shadow': {
                      'type': 'statement_block',
                      'id': 'b',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'NAME', true, '2');
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
                'b',
              );
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': '2',
                        'inputs': {
                          'NAME': {
                            'shadow': {
                              'type': 'statement_block',
                              'id': 'b',
                            },
                          },
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="2">' +
                  '<statement name="NAME">' +
                  '<shadow type="statement_block" id="b"></shadow>' +
                  '</statement>' +
                  '</shadow>' +
                  '</statement>' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': '1',
              });
              assertNextHasBlock(parent, true, '1');
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': '2',
              });
              assertNextHasBlock(parent, true, '2');
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': '2',
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="2"></shadow>' +
                  '</next>' +
                  '</block>',
              );
            });

            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': '1',
                'next': {
                  'shadow': {
                    'type': 'stack_block',
                    'id': 'a',
                  },
                },
              });
              assertNextHasBlock(parent, true, '1');
              assertNextHasBlock(parent.getNextBlock(), true, 'a');
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'id': '2',
                'next': {
                  'shadow': {
                    'type': 'stack_block',
                    'id': 'b',
                  },
                },
              });
              assertNextHasBlock(parent, true, '2');
              assertNextHasBlock(parent.getNextBlock(), true, 'b');
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': '2',
                      'next': {
                        'shadow': {
                          'type': 'stack_block',
                          'id': 'b',
                        },
                      },
                    },
                  },
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '<next>' +
                  '<shadow type="stack_block" id="2">' +
                  '<next>' +
                  '<shadow type="stack_block" id="b"></shadow>' +
                  '</next>' +
                  '</shadow>' +
                  '</next>' +
                  '</block>',
              );
            });
          });

          suite('Remove - No Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStatementBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStackBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                  },
                },
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlocks(this.workspace);
              parent.getInput('INPUT').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'INPUT');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              parent.getInput('NAME').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'NAME');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlocks(this.workspace);
              parent.nextConnection.setShadowState(null);
              assertNextNotHasBlock(parent);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '</block>',
              );
            });
          });

          suite('Remove - Block Connected', function () {
            // These are defined separately in each suite.
            function createRowBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'row_block',
                  'id': 'id0',
                  'inputs': {
                    'INPUT': {
                      'shadow': {
                        'type': 'row_block',
                        'id': 'id1',
                      },
                      'block': {
                        'type': 'row_block',
                        'id': 'id2',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStatementBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'statement_block',
                  'id': 'id0',
                  'inputs': {
                    'NAME': {
                      'shadow': {
                        'type': 'statement_block',
                        'id': 'id1',
                      },
                      'block': {
                        'type': 'statement_block',
                        'id': 'id2',
                      },
                    },
                  },
                },
                workspace,
              );
            }

            function createStackBlocks(workspace) {
              return Blockly.serialization.blocks.append(
                {
                  'type': 'stack_block',
                  'id': 'id0',
                  'next': {
                    'shadow': {
                      'type': 'stack_block',
                      'id': 'id1',
                    },
                    'block': {
                      'type': 'stack_block',
                      'id': 'id2',
                    },
                  },
                },
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlocks(this.workspace);
              parent.getInput('INPUT').connection.setShadowState(null);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputNotHasBlock(parent, 'INPUT');
              assertSerialization(
                parent,
                {
                  'type': 'row_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="row_block" id="id0">' +
                  '</block>',
              );
            });

            test('Statement', function () {
              const parent = createStatementBlocks(this.workspace);
              parent.getInput('NAME').connection.setShadowState(null);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputNotHasBlock(parent, 'NAME');
              assertSerialization(
                parent,
                {
                  'type': 'statement_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="statement_block" id="id0">' +
                  '</block>',
              );
            });

            test('Next', function () {
              const parent = createStackBlocks(this.workspace);
              parent.nextConnection.setShadowState(null);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextNotHasBlock(parent);
              assertSerialization(
                parent,
                {
                  'type': 'stack_block',
                  'id': 'id0',
                },
                '<block xmlns="https://developers.google.com/blockly/xml" ' +
                  'type="stack_block" id="id0">' +
                  '</block>',
              );
            });
          });

          suite('Add - Connect & Disconnect - Remove', function () {
            // These are defined separately in each suite.
            function createRowBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'row_block'},
                workspace,
              );
            }

            function createStatementBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'statement_block'},
                workspace,
              );
            }

            function createStackBlock(workspace) {
              return Blockly.serialization.blocks.append(
                {'type': 'stack_block'},
                workspace,
              );
            }

            test('Value', function () {
              const parent = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.setShadowState({'type': 'row_block'});
              assertInputHasBlock(parent, 'INPUT', true);
              const child = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.connect(child.outputConnection);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              parent.getInput('INPUT').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'INPUT');
            });

            test('Multiple Value', function () {
              const parent = createRowBlock(this.workspace);
              parent.getInput('INPUT').connection.setShadowState({
                'type': 'row_block',
                'inputs': {
                  'INPUT': {
                    'shadow': {
                      'type': 'row_block',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              const child = createRowBlock(this.workspace);
              parent
                .getInput('INPUT')
                .connection.connect(child.outputConnection);
              assertInputHasBlock(parent, 'INPUT', false);
              parent.getInput('INPUT').connection.disconnect();
              assertInputHasBlock(parent, 'INPUT', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('INPUT'),
                'INPUT',
                true,
              );
              parent.getInput('INPUT').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'INPUT');
            });

            test('Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent
                .getInput('NAME')
                .connection.setShadowState({'type': 'statement_block'});
              assertInputHasBlock(parent, 'NAME', true);
              const child = createStatementBlock(this.workspace);
              parent
                .getInput('NAME')
                .connection.connect(child.previousConnection);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              parent.getInput('NAME').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'NAME');
            });

            test('Multiple Statement', function () {
              const parent = createStatementBlock(this.workspace);
              parent.getInput('NAME').connection.setShadowState({
                'type': 'statement_block',
                'inputs': {
                  'NAME': {
                    'shadow': {
                      'type': 'statement_block',
                    },
                  },
                },
              });
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              const child = createStatementBlock(this.workspace);
              parent
                .getInput('NAME')
                .connection.connect(child.previousConnection);
              assertInputHasBlock(parent, 'NAME', false);
              parent.getInput('NAME').connection.disconnect();
              assertInputHasBlock(parent, 'NAME', true);
              assertInputHasBlock(
                parent.getInputTargetBlock('NAME'),
                'NAME',
                true,
              );
              parent.getInput('NAME').connection.setShadowState(null);
              assertInputNotHasBlock(parent, 'NAME');
            });

            test('Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({'type': 'stack_block'});
              const child = createStatementBlock(this.workspace);
              parent.nextConnection.connect(child.previousConnection);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              parent.nextConnection.setShadowState(null);
              assertNextNotHasBlock(parent);
            });

            test('Multiple Next', function () {
              const parent = createStackBlock(this.workspace);
              parent.nextConnection.setShadowState({
                'type': 'stack_block',
                'next': {
                  'shadow': {
                    'type': 'stack_block',
                  },
                },
              });
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              const child = createStatementBlock(this.workspace);
              parent.nextConnection.connect(child.previousConnection);
              assertNextHasBlock(parent, false);
              parent.nextConnection.disconnect();
              assertNextHasBlock(parent, true);
              assertNextHasBlock(parent.getNextBlock(), true);
              parent.nextConnection.setShadowState(null);
              assertNextNotHasBlock(parent);
            });
          });

          suite('Invalid', function () {
            test('Attach to output', function () {
              const block = this.workspace.newBlock('row_block');
              chai.assert.throws(() =>
                block.outputConnection.setShadowState({'type': 'row_block'}),
              );
            });

            test('Attach to previous', function () {
              const block = this.workspace.newBlock('stack_block');
              chai.assert.throws(() =>
                block.previousConnection.setShadowState({
                  'type': 'stack_block',
                }),
              );
            });

            test('Missing output', function () {
              const block = this.workspace.newBlock('row_block');
              chai.assert.throws(() =>
                block.outputConnection.setShadowState({'type': 'stack_block'}),
              );
            });

            test('Missing previous', function () {
              const block = this.workspace.newBlock('stack_block');
              chai.assert.throws(() =>
                block.previousConnection.setShadowState({'type': 'row_block'}),
              );
            });

            test('Invalid connection checks, output', function () {
              const block = this.workspace.newBlock('logic_operation');
              chai.assert.throws(() =>
                block
                  .getInput('A')
                  .connection.setShadowState({'type': 'math_number'}),
              );
            });

            test('Invalid connection checks, previous', function () {
              Blockly.defineBlocksWithJsonArray([
                {
                  'type': 'stack_checks_block',
                  'message0': '',
                  'previousStatement': 'check 1',
                  'nextStatement': 'check 2',
                },
              ]);
              const block = this.workspace.newBlock('stack_checks_block');
              chai.assert.throws(() =>
                block.nextConnection.setShadowState({
                  'type': 'stack_checks_block',
                }),
              );
            });
          });
        });
      });
    });
  });

  suite('Connect', function () {
    setup(function () {
      this.workspace = new Blockly.Workspace();
      Blockly.defineBlocksWithJsonArray([
        {
          'type': 'stack_block',
          'message0': '%1',
          'args0': [
            {
              'type': 'field_input',
              'name': 'FIELD',
              'text': 'default',
            },
          ],
          'previousStatement': 'check1',
          'nextStatement': 'check1',
        },
        {
          'type': 'stack_block_1to2',
          'message0': '',
          'previousStatement': 'check1',
          'nextStatement': 'check2',
        },
        {
          'type': 'stack_block_2to1',
          'message0': '',
          'previousStatement': 'check2',
          'nextStatement': 'check1',
        },
        {
          'type': 'stack_block_noend',
          'message0': '',
          'previousStatement': 'check1',
        },
        {
          'type': 'row_block',
          'message0': '%1 %2',
          'args0': [
            {
              'type': 'field_input',
              'name': 'FIELD',
              'text': 'default',
            },
            {
              'type': 'input_value',
              'name': 'INPUT',
              'check': 'check1',
            },
          ],
          'output': 'check1',
        },
        {
          'type': 'row_block_1to2',
          'message0': '%1',
          'args0': [
            {
              'type': 'input_value',
              'name': 'INPUT',
              'check': 'check1',
            },
          ],
          'output': 'check2',
        },
        {
          'type': 'row_block_2to1',
          'message0': '%1',
          'args0': [
            {
              'type': 'input_value',
              'name': 'INPUT',
              'check': 'check2',
            },
          ],
          'output': 'check1',
        },
        {
          'type': 'row_block_noend',
          'message0': '',
          'output': 'check1',
        },
        {
          'type': 'row_block_multiple_inputs',
          'message0': '%1 %2',
          'args0': [
            {
              'type': 'input_value',
              'name': 'INPUT',
              'check': 'check1',
            },
            {
              'type': 'input_value',
              'name': 'INPUT2',
              'check': 'check1',
            },
          ],
          'output': 'check1',
        },
        {
          'type': 'output_to_statements',
          'message0': '%1 %2',
          'args0': [
            {
              'type': 'input_statement',
              'name': 'INPUT',
              'check': 'check1',
            },
            {
              'type': 'input_statement',
              'name': 'INPUT2',
              'check': 'check1',
            },
          ],
          'output': 'check1',
        },
        {
          'type': 'statement_block',
          'message0': '%1 %2',
          'args0': [
            {
              'type': 'field_input',
              'name': 'FIELD',
              'text': 'default',
            },
            {
              'type': 'input_statement',
              'name': 'NAME',
              'check': 'check1',
            },
          ],
          'previousStatement': 'check1',
          'nextStatement': 'check1',
        },
        {
          'type': 'statement_block_1to2',
          'message0': '%1',
          'args0': [
            {
              'type': 'input_statement',
              'name': 'NAME',
              'check': 'check1',
            },
          ],
          'previousStatement': 'check1',
          'nextStatement': 'check2',
        },
        {
          'type': 'statement_block_2to1',
          'message0': '%1',
          'args0': [
            {
              'type': 'input_statement',
              'name': 'NAME',
              'check': 'check2',
            },
          ],
          'previousStatement': 'check2',
          'nextStatement': 'check1',
        },
        {
          'type': 'statement_block_noend',
          'message0': '%1',
          'args0': [
            {
              'type': 'input_statement',
              'name': 'NAME',
              'check': 'check1',
            },
          ],
          'previousStatement': 'check1',
        },
      ]);

      // Used to make sure we don't get stray shadow blocks or anything.
      this.assertBlockCount = function (count) {
        chai.assert.equal(this.workspace.getAllBlocks().length, count);
      };
    });

    suite('Disconnect from old parent', function () {
      test('Value', function () {
        const oldParent = this.workspace.newBlock('row_block');
        const newParent = this.workspace.newBlock('row_block');
        const child = this.workspace.newBlock('row_block');

        oldParent.getInput('INPUT').connection.connect(child.outputConnection);
        newParent.getInput('INPUT').connection.connect(child.outputConnection);

        chai.assert.isFalse(
          oldParent.getInput('INPUT').connection.isConnected(),
        );
        this.assertBlockCount(3);
      });

      test('Statement', function () {
        const oldParent = this.workspace.newBlock('statement_block');
        const newParent = this.workspace.newBlock('statement_block');
        const child = this.workspace.newBlock('stack_block');

        oldParent.getInput('NAME').connection.connect(child.previousConnection);
        newParent.getInput('NAME').connection.connect(child.previousConnection);

        chai.assert.isFalse(
          oldParent.getInput('NAME').connection.isConnected(),
        );
        this.assertBlockCount(3);
      });

      test('Next', function () {
        const oldParent = this.workspace.newBlock('stack_block');
        const newParent = this.workspace.newBlock('stack_block');
        const child = this.workspace.newBlock('stack_block');

        oldParent.nextConnection.connect(child.previousConnection);
        newParent.nextConnection.connect(child.previousConnection);

        chai.assert.isFalse(oldParent.nextConnection.isConnected());
        this.assertBlockCount(3);
      });
    });

    suite('Shadow dissolves', function () {
      test('Value', function () {
        const newParent = this.workspace.newBlock('row_block');
        const child = this.workspace.newBlock('row_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="row_block"/>');
        newParent.getInput('INPUT').connection.setShadowDom(xml);
        chai.assert.isTrue(newParent.getInputTargetBlock('INPUT').isShadow());

        newParent.getInput('INPUT').connection.connect(child.outputConnection);

        chai.assert.isFalse(newParent.getInputTargetBlock('INPUT').isShadow());
        this.assertBlockCount(2);
      });

      test('Statement', function () {
        const newParent = this.workspace.newBlock('statement_block');
        const child = this.workspace.newBlock('stack_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="stack_block"/>');
        newParent.getInput('NAME').connection.setShadowDom(xml);
        chai.assert.isTrue(newParent.getInputTargetBlock('NAME').isShadow());

        newParent.getInput('NAME').connection.connect(child.previousConnection);

        chai.assert.isFalse(newParent.getInputTargetBlock('NAME').isShadow());
        this.assertBlockCount(2);
      });

      test('Next', function () {
        const newParent = this.workspace.newBlock('stack_block');
        const child = this.workspace.newBlock('stack_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="stack_block"/>');
        newParent.nextConnection.setShadowDom(xml);
        chai.assert.isTrue(newParent.getNextBlock().isShadow());

        newParent.nextConnection.connect(child.previousConnection);

        chai.assert.isFalse(newParent.getNextBlock().isShadow());
        this.assertBlockCount(2);
      });
    });

    suite('Saving shadow values', function () {
      test('Value', function () {
        const newParent = this.workspace.newBlock('row_block');
        const child = this.workspace.newBlock('row_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="row_block"/>');
        newParent.getInput('INPUT').connection.setShadowDom(xml);
        newParent.getInputTargetBlock('INPUT').setFieldValue('new', 'FIELD');

        newParent.getInput('INPUT').connection.connect(child.outputConnection);
        newParent.getInput('INPUT').connection.disconnect();

        const target = newParent.getInputTargetBlock('INPUT');
        chai.assert.isTrue(target.isShadow());
        chai.assert.equal(target.getFieldValue('FIELD'), 'new');
        this.assertBlockCount(3);
      });

      test('Statement', function () {
        const newParent = this.workspace.newBlock('statement_block');
        const child = this.workspace.newBlock('stack_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="stack_block"/>');
        newParent.getInput('NAME').connection.setShadowDom(xml);
        newParent.getInputTargetBlock('NAME').setFieldValue('new', 'FIELD');

        newParent.getInput('NAME').connection.connect(child.previousConnection);
        newParent.getInput('NAME').connection.disconnect();

        const target = newParent.getInputTargetBlock('NAME');
        chai.assert.isTrue(target.isShadow());
        chai.assert.equal(target.getFieldValue('FIELD'), 'new');
        this.assertBlockCount(3);
      });

      test('Next', function () {
        const newParent = this.workspace.newBlock('stack_block');
        const child = this.workspace.newBlock('stack_block');
        const xml = Blockly.utils.xml.textToDom('<shadow type="stack_block"/>');
        newParent.nextConnection.setShadowDom(xml);
        newParent.getNextBlock().setFieldValue('new', 'FIELD');

        newParent.nextConnection.connect(child.previousConnection);
        newParent.nextConnection.disconnect();

        const target = newParent.getNextBlock();
        chai.assert.isTrue(target.isShadow());
        chai.assert.equal(target.getFieldValue('FIELD'), 'new');
        this.assertBlockCount(3);
      });
    });

    suite('Reattach or bump orphan', function () {
      suite('Value', function () {
        suite('No available spots', function () {
          test('No connection', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('row_block_noend');
            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isFalse(oldChild.outputConnection.isConnected());
          });

          test('All statements', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('output_to_statements');
            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isFalse(oldChild.outputConnection.isConnected());
          });

          test('Bad checks', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('row_block_2to1');
            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isFalse(oldChild.outputConnection.isConnected());
          });

          test('Through different types', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('row_block_2to1');
            const otherChild = this.workspace.newBlock('row_block_1to2');

            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);
            newChild
              .getInput('INPUT')
              .connection.connect(otherChild.outputConnection);

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isFalse(oldChild.outputConnection.isConnected());
          });
        });

        suite('Multiple available spots', function () {
          suite('No shadows', function () {
            test('Top block', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });

            test('Child blocks', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );
              const childX = this.workspace.newBlock('row_block');
              const childY = this.workspace.newBlock('row_block');

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);
              newChild
                .getInput('INPUT')
                .connection.connect(childX.outputConnection);
              newChild
                .getInput('INPUT2')
                .connection.connect(childY.outputConnection);

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });

            test('Spots filled', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );
              const otherChild = this.workspace.newBlock('row_block_noend');

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);
              newChild
                .getInput('INPUT')
                .connection.connect(otherChild.outputConnection);

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });
          });

          suite('Shadows', function () {
            test('Top block', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);
              newChild
                .getInput('INPUT')
                .connection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<xml><shadow type="row_block"/></xml>',
                  ).firstChild,
                );
              newChild
                .getInput('INPUT2')
                .connection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<xml><shadow type="row_block"/></xml>',
                  ).firstChild,
                );

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });

            test('Child blocks', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );
              const childX = this.workspace.newBlock('row_block');
              const childY = this.workspace.newBlock('row_block');

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);
              newChild
                .getInput('INPUT')
                .connection.connect(childX.outputConnection);
              newChild
                .getInput('INPUT2')
                .connection.connect(childY.outputConnection);
              childX
                .getInput('INPUT')
                .connection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<xml><shadow type="row_block"/></xml>',
                  ).firstChild,
                );
              childY
                .getInput('INPUT')
                .connection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<xml><shadow type="row_block"/></xml>',
                  ).firstChild,
                );

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });

            test('Spots filled', function () {
              const parent = this.workspace.newBlock('row_block');
              const oldChild = this.workspace.newBlock('row_block');
              const newChild = this.workspace.newBlock(
                'row_block_multiple_inputs',
              );
              const otherChild = this.workspace.newBlock('row_block_noend');

              parent
                .getInput('INPUT')
                .connection.connect(oldChild.outputConnection);
              newChild
                .getInput('INPUT')
                .connection.connect(otherChild.outputConnection);
              newChild
                .getInput('INPUT2')
                .connection.setShadowDom(
                  Blockly.utils.xml.textToDom(
                    '<xml><shadow type="row_block"/></xml>',
                  ).firstChild,
                );

              parent
                .getInput('INPUT')
                .connection.connect(newChild.outputConnection);

              chai.assert.isTrue(
                parent.getInput('INPUT').connection.isConnected(),
              );
              chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
              chai.assert.isFalse(oldChild.outputConnection.isConnected());
            });
          });
        });

        suite('Single available spot', function () {
          test('No shadows', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('row_block');

            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isTrue(
              newChild.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(newChild.getInputTargetBlock('INPUT'), oldChild);
          });

          test('Shadows', function () {
            const parent = this.workspace.newBlock('row_block');
            const oldChild = this.workspace.newBlock('row_block');
            const newChild = this.workspace.newBlock('row_block');

            parent
              .getInput('INPUT')
              .connection.connect(oldChild.outputConnection);
            newChild
              .getInput('INPUT')
              .connection.setShadowDom(
                Blockly.utils.xml.textToDom(
                  '<xml><shadow type="row_block"/></xml>',
                ).firstChild,
              );

            parent
              .getInput('INPUT')
              .connection.connect(newChild.outputConnection);

            chai.assert.isTrue(
              parent.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('INPUT'), newChild);
            chai.assert.isTrue(
              newChild.getInput('INPUT').connection.isConnected(),
            );
            chai.assert.equal(newChild.getInputTargetBlock('INPUT'), oldChild);
          });
        });
      });

      suite('Statement', function () {
        suite('No shadows', function () {
          test('Simple', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);

            parent
              .getInput('NAME')
              .connection.connect(newChild.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.equal(newChild.getNextBlock(), oldChild);
            this.assertBlockCount(3);
          });

          test('Bad check in between', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild1 = this.workspace.newBlock('stack_block_1to2');
            const newChild2 = this.workspace.newBlock('stack_block_2to1');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            newChild1.nextConnection.connect(newChild2.previousConnection);

            parent
              .getInput('NAME')
              .connection.connect(newChild1.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild1);
            chai.assert.isTrue(newChild2.nextConnection.isConnected());
            chai.assert.equal(newChild2.getNextBlock(), oldChild);
            this.assertBlockCount(4);
          });

          test('Bad check at end', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_1to2');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent
              .getInput('NAME')
              .connection.connect(newChild.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild);
            chai.assert.isFalse(newChild.nextConnection.isConnected());
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(3);
          });

          test('No end connection', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_noend');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent
              .getInput('NAME')
              .connection.connect(newChild.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild);
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(3);
          });
        });

        suite('Shadows', function () {
          test('Simple', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block"/>',
            );
            newChild.nextConnection.setShadowDom(xml);

            parent
              .getInput('NAME')
              .connection.connect(newChild.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.equal(newChild.getNextBlock(), oldChild);
            this.assertBlockCount(3);
          });

          test('Bad check in between', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild1 = this.workspace.newBlock('stack_block_1to2');
            const newChild2 = this.workspace.newBlock('stack_block_2to1');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            newChild1.nextConnection.connect(newChild2.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block"/>',
            );
            newChild2.nextConnection.setShadowDom(xml);

            parent
              .getInput('NAME')
              .connection.connect(newChild1.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild1);
            chai.assert.isTrue(newChild2.nextConnection.isConnected());
            chai.assert.equal(newChild2.getNextBlock(), oldChild);
            this.assertBlockCount(4);
          });

          test('Bad check at end', function () {
            const parent = this.workspace.newBlock('statement_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_1to2');
            parent
              .getInput('NAME')
              .connection.connect(oldChild.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block_2to1"/>',
            );
            newChild.nextConnection.setShadowDom(xml);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent
              .getInput('NAME')
              .connection.connect(newChild.previousConnection);

            chai.assert.isTrue(
              parent.getInput('NAME').connection.isConnected(),
            );
            chai.assert.equal(parent.getInputTargetBlock('NAME'), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.isTrue(newChild.getNextBlock().isShadow());
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(4);
          });
        });
      });

      suite('Next', function () {
        suite('No shadows', function () {
          test('Simple', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block');
            parent.nextConnection.connect(oldChild.previousConnection);

            parent.nextConnection.connect(newChild.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.equal(newChild.getNextBlock(), oldChild);
            this.assertBlockCount(3);
          });

          test('Bad check in between', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild1 = this.workspace.newBlock('stack_block_1to2');
            const newChild2 = this.workspace.newBlock('stack_block_2to1');
            parent.nextConnection.connect(oldChild.previousConnection);
            newChild1.nextConnection.connect(newChild2.previousConnection);

            parent.nextConnection.connect(newChild1.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild1);
            chai.assert.isTrue(newChild2.nextConnection.isConnected());
            chai.assert.equal(newChild2.getNextBlock(), oldChild);
            this.assertBlockCount(4);
          });

          test('Bad check at end', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_1to2');
            parent.nextConnection.connect(oldChild.previousConnection);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent.nextConnection.connect(newChild.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild);
            chai.assert.isFalse(newChild.nextConnection.isConnected());
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(3);
          });

          test('No end connection', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_noend');
            parent.nextConnection.connect(oldChild.previousConnection);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent.nextConnection.connect(newChild.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild);
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(3);
          });
        });

        suite('Shadows', function () {
          test('Simple', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block');
            parent.nextConnection.connect(oldChild.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block"/>',
            );
            newChild.nextConnection.setShadowDom(xml);

            parent.nextConnection.connect(newChild.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.equal(newChild.getNextBlock(), oldChild);
            this.assertBlockCount(3);
          });

          test('Bad check in between', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild1 = this.workspace.newBlock('stack_block_1to2');
            const newChild2 = this.workspace.newBlock('stack_block_2to1');
            parent.nextConnection.connect(oldChild.previousConnection);
            newChild1.nextConnection.connect(newChild2.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block"/>',
            );
            newChild2.nextConnection.setShadowDom(xml);

            parent.nextConnection.connect(newChild1.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild1);
            chai.assert.isTrue(newChild2.nextConnection.isConnected());
            chai.assert.equal(newChild2.getNextBlock(), oldChild);
            this.assertBlockCount(4);
          });

          test('Bad check at end', function () {
            const parent = this.workspace.newBlock('stack_block');
            const oldChild = this.workspace.newBlock('stack_block');
            const newChild = this.workspace.newBlock('stack_block_1to2');
            parent.nextConnection.connect(oldChild.previousConnection);
            const xml = Blockly.utils.xml.textToDom(
              '<shadow type="stack_block_2to1"/>',
            );
            newChild.nextConnection.setShadowDom(xml);
            const spy = sinon.spy(
              oldChild.previousConnection,
              'onFailedConnect',
            );

            parent.nextConnection.connect(newChild.previousConnection);

            chai.assert.isTrue(parent.nextConnection.isConnected());
            chai.assert.equal(parent.getNextBlock(), newChild);
            chai.assert.isTrue(newChild.nextConnection.isConnected());
            chai.assert.isTrue(newChild.getNextBlock().isShadow());
            chai.assert.isTrue(spy.calledOnce);
            this.assertBlockCount(4);
          });
        });
      });
    });
  });
});
