aurelia/aurelia

View on GitHub
packages/__tests__/src/3-runtime-html/switch.spec.ts

Summary

Maintainability
A
0 mins
Test Coverage
import {
  DefaultLogEvent,
  DI,
  IContainer,
  ILogger,
  ISink,
  LoggerConfiguration,
  LogLevel,
  pascalCase,
  Registration,
  resolve,
  sink,
} from '@aurelia/kernel';
import {
  Scope,
  AuSlot,
  type BindingBehaviorInstance,
  type IBinding,
  Case,
  Controller,
  bindingBehavior,
  customElement,
  CustomElement,
  Repeat,
  Switch,
  Aurelia,
  IPlatform,
  ICustomElementViewModel,
  ICustomElementController,
  bindable,
  INode,
  valueConverter,
} from '@aurelia/runtime-html';
import {
  assert,
  createFixture,
  TestContext,
} from '@aurelia/testing';
import {
  createSpecFunction,
  TestExecutionContext,
  TestFunction,
} from '../util.js';

describe('3-runtime-html/switch.spec.ts', function () {

  const enum Status {
    unknown = 'unknown',
    received = 'received',
    processing = 'processing',
    dispatched = 'dispatched',
    delivered = 'delivered',
  }

  const enum StatusNum {
    unknown = 0,
    received = 1,
    processing = 2,
    dispatched = 3,
    delivered = 4,
  }

  const InitialStatus = DI.createInterface<Status>('InitialStatus');
  const InitialStatusNum = DI.createInterface<StatusNum>('InitialStatusNum');

  class Config {
    public constructor(
      public hasPromise: boolean,
      public hasTimeout: boolean,
      public wait: () => Promise<void>,
    ) { }

    public toString(): string {
      return `{${this.hasPromise ? this.wait.toString() : 'noWait'}}`;
    }
  }

  const IConfig = DI.createInterface<Config>('Config', x => x.singleton(Config));

  function createComponentType(name: string, template: string, bindables: string[] = []) {
    @customElement({ name, template, bindables })
    class Component implements ICustomElementViewModel {
      private logger: ILogger;
      public readonly $controller: ICustomElementController<this>;
      @bindable
      private readonly ceId: unknown = null;
      private readonly config: Config = resolve(IConfig);
      private readonly $logger: ILogger = resolve(ILogger);
      public constructor() {
        const node = resolve(INode);
        const ceId = (node as HTMLElement).dataset.ceId;
        if (ceId) {
          (this.logger = resolve(ILogger).scopeTo(`${name}-${ceId}`)).debug('ctor');
          delete (node as HTMLElement).dataset.ceId;
        }
      }

      public async binding(): Promise<void> {
        this.logger ??= this.ceId === null ? this.$logger.scopeTo(name) : this.$logger.scopeTo(`${name}-${this.ceId}`);
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('binding');
      }

      public async bound(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('bound');
      }

      public async attaching(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('attaching');
      }

      public async attached(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('attached');
      }

      public async detaching(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('detaching');
      }

      public async unbinding(): Promise<void> {
        if (this.config.hasPromise) {
          await this.config.wait();
        }

        this.logger.debug('unbinding');
      }
    }

    Reflect.defineProperty(Component, 'name', {
      writable: false,
      enumerable: false,
      configurable: true,
      value: pascalCase(name),
    });

    return Component;
  }

  @sink({ handles: [LogLevel.debug] })
  class DebugLog implements ISink {
    public readonly log: string[] = [];
    public handleEvent(event: DefaultLogEvent): void {
      this.log.push(`${event.scope.join('.')}.${event.message}`);
    }
    public clear() {
      this.log.length = 0;
    }
  }

  interface TestSetupContext {
    template: string;
    registrations: any[];
    initialStatus: Status;
    initialStatusNum: StatusNum;
    expectedStopLog: string[];
    verifyStopCallsAsSet: boolean;
  }
  class SwitchTestExecutionContext implements TestExecutionContext<any> {
    private _scheduler: IPlatform;
    private readonly _log: DebugLog;
    private changeId: number = 0;
    public constructor(
      public ctx: TestContext,
      public container: IContainer,
      public host: HTMLElement,
      public app: App | null,
      public controller: Controller,
      public error: Error | null,
    ) {
      this._log = container.get(ILogger).sinks.find((s) => s instanceof DebugLog) as DebugLog;
    }
    public get platform(): IPlatform { return this._scheduler ?? (this._scheduler = this.container.get(IPlatform)); }
    public get log() {
      return this._log.log;
    }
    public getSwitches(controller = this.controller) {
      return controller.children
        .reduce((acc: Switch[], c) => {
          const vm = c.viewModel;
          if (vm instanceof Switch) {
            acc.push(vm);
          }
          return acc;
        }, []);
    }
    public clear() {
      this._log.clear();
    }
    public async wait($switch: Switch): Promise<void> {
      const promise = $switch.promise;
      await promise;
      if ($switch.promise !== promise) {
        await this.wait($switch);
      }
    }

    public assertCalls(expected: (string | number)[], message: string = '') {
      assert.deepStrictEqual(this.log, this.transformCalls(expected), message);
    }

    public assertCallSet(expected: (string | number)[], message: string = '') {
      expected = this.transformCalls(expected);
      const actual = this.log;
      assert.strictEqual(actual.length, expected.length, `${message} - calls.length`);
      assert.strictEqual(actual.filter((c) => !expected.includes(c)).length, 0, `${message} - calls set equality \n actual:\t${actual} \n expected:\t ${expected}`);
    }

    public async assertChange($switch: Switch, act: () => void, expectedHtml: string, expectedLog: (string | number)[]) {
      this.clear();
      act();
      await this.wait($switch);
      const change = `change${++this.changeId}`;
      assert.html.innerEqual(this.host, expectedHtml, `${change} innerHTML`);
      this.assertCalls(expectedLog, change);
    }

    private transformCalls(calls: (string | number)[]) {
      let cases: Case[];
      const getCases = () => cases ?? (cases = this.getSwitches().flatMap((s) => s['cases']));
      return calls.map((item) => typeof item === 'string' ? item : `Case-#${getCases()[item - 1].id}.isMatch()`);
    }
  }

  async function testSwitch(
    testFunction: TestFunction<SwitchTestExecutionContext>,
    {
      template,
      registrations = [],
      initialStatus = Status.unknown,
      initialStatusNum = StatusNum.unknown,
      expectedStopLog,
      verifyStopCallsAsSet = false,
    }: Partial<TestSetupContext> = {}
  ) {
    const ctx = TestContext.create();

    const host = ctx.doc.createElement('div');
    ctx.doc.body.appendChild(host);

    const container = ctx.container;

    const au = new Aurelia(container);
    let error: Error | null = null;
    let app: App | null = null;
    let controller: Controller = null!;
    try {
      await au
        .register(
          LoggerConfiguration.create({ level: LogLevel.trace, sinks: [DebugLog] }),
          ...registrations,
          Registration.instance(InitialStatus, initialStatus),
          Registration.instance(InitialStatusNum, initialStatusNum),
          ToStatusStringValueConverter,
          NoopBindingBehavior,
        )
        .app({
          host,
          component: CustomElement.define({ name: 'app', template }, App)
        })
        .start();
      app = au.root.controller.viewModel as App;
      controller = au.root.controller! as unknown as Controller;
    } catch (e) {
      error = e;
    }

    const testCtx = new SwitchTestExecutionContext(ctx, container, host, app, controller, error);
    await testFunction(testCtx);

    if (error === null) {
      testCtx.clear();
      await au.stop();
      assert.html.innerEqual(host, '', 'post-detach innerHTML');
      if (verifyStopCallsAsSet) {
        testCtx.assertCallSet(expectedStopLog);
      } else {
        testCtx.assertCalls(expectedStopLog, 'stop lifecycle calls');
      }
    }
    ctx.doc.body.removeChild(host);
  }
  const $it = createSpecFunction(testSwitch);

  @valueConverter('toStatusString')
  class ToStatusStringValueConverter {
    public toView(value: StatusNum): string {
      switch (value) {
        case StatusNum.received:
          return Status.received;
        case StatusNum.processing:
          return Status.processing;
        case StatusNum.dispatched:
          return Status.dispatched;
        case StatusNum.delivered:
          return Status.delivered;
        case StatusNum.unknown:
          return Status.unknown;
      }
    }
  }

  @bindingBehavior('noop')
  class NoopBindingBehavior implements BindingBehaviorInstance {
    public bind(_scope: Scope, _binding: IBinding): void {
      return;
    }
    public unbind(_scope: Scope, _binding: IBinding): void {
      return;
    }
  }

  class App {
    public status1: Status = Status.received;
    public status2: Status = Status.processing;
    public statuses: Status[] = [Status.received, Status.processing];
    public status: Status = resolve(InitialStatus);
    public statusNum: StatusNum = resolve(InitialStatusNum);
  }

  function getActivationSequenceFor(name: string | string[], withCtor: boolean = false) {
    return typeof name === 'string'
      ? [...(withCtor ? [`${name}.ctor`] : []), `${name}.binding`, `${name}.bound`, `${name}.attaching`, `${name}.attached`]
      : [...(withCtor ? ['ctor'] : []), 'binding', 'bound', 'attaching', 'attached'].flatMap(x => name.map(n => `${n}.${x}`));
  }
  function getDeactivationSequenceFor(name: string | string[]) {
    return typeof name === 'string'
      ? [`${name}.detaching`, `${name}.unbinding`]
      : ['detaching', 'unbinding'].flatMap(x => name.map(n => `${n}.${x}`));
  }

  class TestData implements TestSetupContext {
    public readonly initialStatus: Status;
    public readonly template: string;
    public readonly registrations: any[];
    public readonly initialStatusNum: StatusNum;
    public readonly verifyStopCallsAsSet: boolean;
    public constructor(
      public readonly name: string,
      {
        initialStatus = Status.unknown,
        initialStatusNum = StatusNum.unknown,
        registrations = [],
        template,
        verifyStopCallsAsSet = false,
      }: Partial<TestSetupContext>,
      public readonly config: Config | null = null,
      public readonly expectedInnerHtml: string = '',
      public readonly expectedStartLog: (string | number)[],
      public readonly expectedStopLog: string[],
      public readonly additionalAssertions: ((ctx: SwitchTestExecutionContext) => Promise<void> | void) | null = null,
      public readonly only: boolean = false,
    ) {
      this.initialStatus = initialStatus;
      this.initialStatusNum = initialStatusNum;
      this.registrations = [
        Registration.instance(Config, config),
        createComponentType('case-host', '<au-slot>'),
        createComponentType('default-case-host', '<au-slot>'),
        ...registrations,
      ];
      this.template = template;
      this.verifyStopCallsAsSet = verifyStopCallsAsSet;
    }
  }

  function createWaiter(ms: number): () => Promise<void> {
    function wait(): Promise<void> {
      return new Promise(function (resolve) {
        setTimeout(function () {
          resolve();
        }, ms);
      });
    }

    wait.toString = function () {
      return `setTimeout(cb,${ms})`;
    };

    return wait;
  }
  function noop(): Promise<void> {
    return;
  }

  noop.toString = function () {
    return 'Promise.resolve()';
  };

  const configFactories = [
    function () {
      return new Config(false, false, noop);
    },
    function () {
      return new Config(true, false, noop);
    },
    function () {
      return new Config(true, true, createWaiter(0));
    },
    function () {
      return new Config(true, true, createWaiter(5));
    },
  ];

  function* getTestData() {
    function wrap(content: string, isDefault: boolean = false) {
      const host = isDefault ? 'default-case-host' : 'case-host';
      return `<${host}>${content}</${host}>`;
    }
    for (const config of configFactories) {
      const MyEcho = createComponentType('my-echo', `Echoed '\${message}'`, ['message']);

      yield new TestData(
        'works for simple switch-case',
        {
          initialStatus: Status.processing,
          template: `
          <template>
            <template switch.bind="status">
              <case-host case="received"   ce-id="1">Order received.</case-host>
              <case-host case="dispatched" ce-id="2">On the way.</case-host>
              <case-host case="processing" ce-id="3">Processing your order.</case-host>
              <case-host case="delivered"  ce-id="4">Delivered.</case-host>
            </template>
          </template>`,
        },
        config(),
        wrap('Processing your order.'),
        [1, 2, 3, ...getActivationSequenceFor('case-host-3')],
        getDeactivationSequenceFor('case-host-3'),
      );

      yield new TestData(
        'reacts to switch value change + deferred view-instantiation assertion',
        {
          initialStatus: Status.dispatched,
          template: `
          <template>
            <template switch.bind="status">
              <case-host case="received"   data-ce-id="1">Order received.</case-host>
              <case-host case="dispatched" data-ce-id="2">On the way.</case-host>
              <case-host case="processing" data-ce-id="3">Processing your order.</case-host>
              <case-host case="delivered"  data-ce-id="4">Delivered.</case-host>
            </template>
          </template>`
        },
        config(),
        wrap('On the way.'),
        [1, 2, ...getActivationSequenceFor('case-host-2', true)],
        getDeactivationSequenceFor('case-host-2'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            wrap('Delivered.'),
            [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-4', true)]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.unknown; },
            '',
            [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-4')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.received; },
            wrap('Order received.'),
            [1, ...getActivationSequenceFor('case-host-1', true)]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.dispatched; },
            wrap('On the way.'),
            [1, 2, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-2')]
          );
        },
      );

      const templateWithDefaultCase = `
    <template>
      <template switch.bind="status">
        <case-host case="received"   ce-id="1">Order received.</case-host>
        <case-host case="dispatched" ce-id="2">On the way.</case-host>
        <case-host case="processing" ce-id="3">Processing your order.</case-host>
        <case-host case="delivered"  ce-id="4">Delivered.</case-host>
        <default-case-host default-case ce-id="1">Not found.</default-case-host>
      </template>
    </template>`;

      yield new TestData(
        'supports default-case',
        {
          initialStatus: Status.unknown,
          template: templateWithDefaultCase
        },
        config(),
        wrap('Not found.', true),
        [1, 2, 3, 4, ...getActivationSequenceFor('default-case-host-1')],
        getDeactivationSequenceFor('default-case-host-1'),
      );

      yield new TestData(
        'reacts to switch value change - default case',
        {
          initialStatus: Status.dispatched,
          template: templateWithDefaultCase,
        },
        config(),
        wrap('On the way.'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-4'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.unknown; },
            wrap('Not found.', true),
            [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('default-case-host-1')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            wrap('Delivered.'),
            [1, 2, 3, 4, ...getDeactivationSequenceFor('default-case-host-1'), ...getActivationSequenceFor('case-host-4')]
          );
        }
      );

      yield new TestData(
        'supports case.bind - #1',
        {
          initialStatus: Status.processing,
          template: `
    <template>
      <template switch.bind="true">
        <case-host case.bind="status === 'received'"   ce-id="1">Order received.</case-host>
        <case-host case.bind="status === 'processing'" ce-id="2">Processing your order.</case-host>
        <case-host case.bind="status === 'dispatched'" ce-id="3">On the way.</case-host>
        <case-host case.bind="status === 'delivered'"  ce-id="4">Delivered.</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Processing your order.'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-3'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.dispatched; },
            wrap('On the way.'),
            [2, ...getDeactivationSequenceFor('case-host-2'), 3, ...getActivationSequenceFor('case-host-3')]
          );
        }
      );

      yield new TestData(
        'supports case.bind - #2',
        {
          initialStatus: Status.processing,
          template: `
    <template>
      <template switch.bind="status">
        <case-host case.bind="status1" ce-id="1">Order received.</case-host>
        <case-host case.bind="status2" ce-id="2">Processing your order.</case-host>
        <case-host case="dispatched"   ce-id="3">On the way.</case-host>
        <case-host case="delivered"    ce-id="4">Delivered.</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Processing your order.'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.dispatched; },
            wrap('On the way.'),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-3')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status1 = Status.processing; },
            wrap('On the way.'),
            [1]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            wrap('Order received.'),
            [1, ...getDeactivationSequenceFor('case-host-3'), ...getActivationSequenceFor('case-host-1')]
          );
        }
      );

      yield new TestData(
        'supports case.bind - #3',
        {
          template: `
    <template>
      <let num.bind="9"></let>
      <template switch.bind="true">
        <case-host case.bind="num % 3 === 0 && num % 5 === 0" ce-id="1">FizzBuzz</case-host>
        <case-host case.bind="num % 3 === 0" ce-id="2">Fizz</case-host>
        <case-host case.bind="num % 5 === 0" ce-id="3">Buzz</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Fizz'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.controller.scope.overrideContext.num = 49; },
            '',
            [2, ...getDeactivationSequenceFor('case-host-2')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.controller.scope.overrideContext.num = 15; },
            wrap('FizzBuzz'),
            [1, ...getActivationSequenceFor('case-host-1'), 2, 3]
          );
        }
      );

      yield new TestData(
        'supports multi-case',
        {
          initialStatus: Status.processing,
          template: `
    <template>
      <template switch.bind="status">
        <case-host case.bind="['received', 'processing']" ce-id="1">Processing.</case-host>
        <case-host case="dispatched" ce-id="2">On the way.</case-host>
        <case-host case="delivered"  ce-id="3">Delivered.</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Processing.'),
        [1, ...getActivationSequenceFor('case-host-1')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.dispatched; },
            wrap('On the way.'),
            [1, 2, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-2')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.received; },
            wrap('Processing.'),
            [1, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-1')]
          );
        }
      );

      yield new TestData(
        'supports multi-case collection change - #1',
        {
          initialStatus: Status.received,
          template: `
    <template>
      <template switch.bind="status">
        <case-host case.bind="statuses" ce-id="1">Processing.</case-host>
        <case-host case="dispatched"    ce-id="2">On the way.</case-host>
        <case-host case="delivered"     ce-id="3">Delivered.</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Processing.'),
        [1, ...getActivationSequenceFor('case-host-1')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.dispatched; },
            wrap('On the way.'),
            [1, 2, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-2')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statuses = [Status.dispatched]; },
            wrap('Processing.'),
            [1, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-1')]
          );
        }
      );

      yield new TestData(
        'supports multi-case collection change - #2',
        {
          initialStatus: Status.dispatched,
          template: `
    <template>
      <template switch.bind="status">
        <case-host case.bind="statuses" ce-id="1">Processing.</case-host>
        <case-host case="dispatched"    ce-id="2">On the way.</case-host>
        <case-host case="delivered"     ce-id="3">Delivered.</case-host>
        <default-case-host default-case ce-id="1">Unknown.</default-case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('On the way.'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statuses = [Status.dispatched]; },
            wrap('Processing.'),
            [1, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-1')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.unknown; },
            wrap('Unknown.', true),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('default-case-host-1')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statuses = [ctx.app.status = Status.delivered]; },
            wrap('Processing.'),
            [1, 2, 3, ...getDeactivationSequenceFor('default-case-host-1'), ...getActivationSequenceFor('case-host-3'), 1, ...getDeactivationSequenceFor('case-host-3'), ...getActivationSequenceFor('case-host-1')]
          );
        }
      );

      yield new TestData(
        'supports multi-case collection mutation',
        {
          initialStatus: Status.dispatched,
          template: `
    <template>
      <template switch.bind="status">
        <case-host case.bind="statuses" ce-id="1">Processing.</case-host>
        <case-host case="dispatched"    ce-id="2">On the way.</case-host>
        <case-host case="delivered"     ce-id="3">Delivered.</case-host>
        <default-case-host default-case ce-id="1">Unknown.</default-case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('On the way.'),
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-1'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statuses.push(Status.dispatched); },
            wrap('Processing.'),
            [1, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-1')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.unknown; },
            wrap('Unknown.', true),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('default-case-host-1')]
          );
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statuses.push(ctx.app.status = Status.delivered); },
            wrap('Processing.'),
            [1, 2, 3, ...getDeactivationSequenceFor('default-case-host-1'), ...getActivationSequenceFor('case-host-3'), 1, ...getDeactivationSequenceFor('case-host-3'), ...getActivationSequenceFor('case-host-1')]
          );
        }
      );

      const fallThroughTemplate = `
      <template>
        <template switch.bind="status">
          <case-host case="received"                                   ce-id="1">Order received.</case-host>
          <case-host case="value:dispatched; fall-through.bind:true"   ce-id="2">On the way.</case-host>
          <case-host case="value.bind:'processing'; fall-through:true" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"                                  ce-id="4">Delivered.</case-host>
        </template>
      </template>`;

      yield new TestData(
        'supports fall-through #1',
        {
          initialStatus: Status.dispatched,
          template: fallThroughTemplate,
        },
        config(),
        `${wrap('On the way.')} ${wrap('Processing your order.')} ${wrap('Delivered.')}`,
        [1, 2, ...getActivationSequenceFor(['case-host-2', 'case-host-3', 'case-host-4'])],
        getDeactivationSequenceFor('case-host-4'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            wrap('Delivered.'),
            [1, 2, 3, 4, ...getDeactivationSequenceFor(['case-host-2', 'case-host-3'])]
          );
        }
      );

      yield new TestData(
        'supports fall-through #2',
        {
          initialStatus: Status.delivered,
          template: fallThroughTemplate,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor(['case-host-3', 'case-host-4']),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            `${wrap('Processing your order.')} ${wrap('Delivered.')}`,
            [1, 2, 3, ...getActivationSequenceFor('case-host-3')],
          );
        }
      );

      yield new TestData(
        'supports fall-through #3',
        {
          initialStatus: Status.delivered,
          template: `
    <template>
      <template switch.bind="true">
        <case-host case.bind="status === 'received'"                                 ce-id="1">Order received.</case-host>
        <case-host case="value.bind:status === 'processing'; fall-through:true"      ce-id="2">Processing your order.</case-host>
        <case-host case="value.bind:status === 'dispatched'; fall-through.bind:true" ce-id="3">On the way.</case-host>
        <case-host case.bind="status === 'delivered'"                                ce-id="4">Delivered.</case-host>
      </template>
    </template>`,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor(['case-host-2', 'case-host-3', 'case-host-4']),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            `${wrap('Processing your order.')} ${wrap('On the way.')} ${wrap('Delivered.')}`,
            [2, ...getActivationSequenceFor(['case-host-2', 'case-host-3']), 4]
          );
        }
      );

      yield new TestData(
        'works without case',
        {
          initialStatus: Status.processing,
          template: `
        <template>
          <div switch.bind="status">
            the curious case of \${status}
          </div>
        </template>`,
        },
        config(),
        '<div> the curious case of processing </div>',
        [],
        [],
        async (ctx) => {
          ctx.app.status = Status.delivered;
          ctx.platform.domWriteQueue.flush();
          assert.html.innerEqual(ctx.host, '<div> the curious case of delivered </div>', 'change innerHTML1');
        }
      );

      yield new TestData(
        'supports non-case elements',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="4">Delivered.</case-host>
          <span>foobar</span>
          <span if.bind="true">foo</span><span else>bar</span>
          <span if.bind="false">foo</span><span else>bar</span>
          <span repeat.for="i of 3">\${i}</span>
          <my-echo message="awesome possum"></my-echo>
        </template>
      </template>`,
          registrations: [MyEcho],
        },
        config(),
        `${wrap('Delivered.')} <span>foobar</span> <span>foo</span> <span>bar</span> <span>0</span><span>1</span><span>2</span> <my-echo>Echoed 'awesome possum'</my-echo>`,
        [...getActivationSequenceFor('my-echo'), 1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor(['case-host-4', 'my-echo']),
      );

      yield new TestData(
        'works with value converter for switch expression',
        {
          initialStatusNum: StatusNum.delivered,
          template: `
      <template>
        <template switch.bind="statusNum | toStatusString">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="4">Delivered.</case-host>
        </template>
      </template>`,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor('case-host-3'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.statusNum = StatusNum.processing; },
            wrap('Processing your order.'),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-3')]
          );
        }
      );

      yield new TestData(
        'works with value converter for case expression',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case.bind="1 | toStatusString" ce-id="1">Order received.</case-host>
          <case-host case.bind="3 | toStatusString" ce-id="2">On the way.</case-host>
          <case-host case.bind="2 | toStatusString" ce-id="3">Processing your order.</case-host>
          <case-host case.bind="4 | toStatusString" ce-id="4">Delivered.</case-host>
        </template>
      </template>`,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor('case-host-3'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            wrap('Processing your order.'),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-3')]
          );
        }
      );

      yield new TestData(
        'works with bindingBehavior for switch expression',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <template switch.bind="status & noop">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="4">Delivered.</case-host>
        </template>
      </template>`,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor('case-host-3'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            wrap('Processing your order.'),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-3')]
          );
        }
      );

      yield new TestData(
        'works with bindingBehavior for case expression',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case.bind="'received' & noop"   ce-id="1">Order received.</case-host>
          <case-host case.bind="'dispatched' & noop" ce-id="2">On the way.</case-host>
          <case-host case.bind="'processing' & noop" ce-id="3">Processing your order.</case-host>
          <case-host case.bind="'delivered' & noop"  ce-id="4">Delivered.</case-host>
        </template>
      </template>`,
        },
        config(),
        wrap('Delivered.'),
        [1, 2, 3, 4, ...getActivationSequenceFor('case-host-4')],
        getDeactivationSequenceFor('case-host-3'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.processing; },
            wrap('Processing your order.'),
            [1, 2, 3, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-3')]
          );
        }
      );

      yield new TestData(
        'works with repeater - switch expression',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <div repeat.for="s of ['received', 'dispatched']">
          <template switch.bind="s">
            <case-host case="received"   ce-id.bind="$index * 4 + 1">Order received.</case-host>
            <case-host case="dispatched" ce-id.bind="$index * 4 + 2">On the way.</case-host>
            <case-host case="processing" ce-id.bind="$index * 4 + 3">Processing your order.</case-host>
            <case-host case="delivered"  ce-id.bind="$index * 4 + 4">Delivered.</case-host>
          </template>
        </div>
      </template>`,
        },
        config(),
        `<div> ${wrap('Order received.')} </div><div> ${wrap('On the way.')} </div>`,
        null,
        getDeactivationSequenceFor(['case-host-1', 'case-host-6']),
        (ctx) => {
          const switches = (ctx.controller.children[0] as Controller<Repeat>).viewModel
            .views
            .map((v) => v.children[0].viewModel as Switch);
          ctx.assertCallSet([
            `Case-#${switches[0]['cases'][0].id}.isMatch()`,
            ...getActivationSequenceFor('case-host-1'),
            `Case-#${switches[1]['cases'][0].id}.isMatch()`,
            `Case-#${switches[1]['cases'][1].id}.isMatch()`,
            ...getActivationSequenceFor('case-host-6')
          ], 'post-start lifecycle calls');
        },
      );

      yield new TestData(
        '*[switch][repeat.for] works',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <div repeat.for="s of ['received', 'dispatched']" switch.bind="s">
          <case-host case="received"   ce-id.bind="$index * 4 + 1">Order received.</case-host>
          <case-host case="dispatched" ce-id.bind="$index * 4 + 2">On the way.</case-host>
          <case-host case="processing" ce-id.bind="$index * 4 + 3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id.bind="$index * 4 + 4">Delivered.</case-host>
        </div>
      </template>`,
        },
        config(),
        `<div> ${wrap('Order received.')} </div><div> ${wrap('On the way.')} </div>`,
        null,
        getDeactivationSequenceFor(['case-host-1', 'case-host-6']),
        (ctx) => {
          const switches = (ctx.controller.children[0] as Controller<Repeat>).viewModel
            .views
            .map((v) => v.children[0].viewModel as Switch);
          ctx.assertCallSet([
            `Case-#${switches[0]['cases'][0].id}.isMatch()`,
            ...getActivationSequenceFor('case-host-1'),
            `Case-#${switches[1]['cases'][0].id}.isMatch()`,
            `Case-#${switches[1]['cases'][1].id}.isMatch()`,
            ...getActivationSequenceFor('case-host-6')
          ], 'post-start lifecycle calls');
        }
      );

      // tag: nonsense example
      yield new TestData(
        '*[switch][if] works',
        {
          initialStatus: Status.delivered,
          template: `
        <div if.bind="true" switch.bind="status">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="4">Delivered.</case-host>
        </div>
        <div if.bind="false" switch.bind="status">
          <case-host case="received"   ce-id="5">Order received.</case-host>
          <case-host case="dispatched" ce-id="6">On the way.</case-host>
          <case-host case="processing" ce-id="7">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="8">Delivered.</case-host>
        </div>
      `,
        },
        config(),
        `<div> ${wrap('Delivered.')} </div>`,
        null,
        getDeactivationSequenceFor('case-host-4'),
      );

      // tag: nonsense example
      yield new TestData(
        '*[case][if=true]',
        {
          initialStatus: Status.delivered,
          template: `
        <div switch.bind="status">
          <case-host case="processing" ce-id="1">Processing your order.</case-host>
          <case-host case="delivered" if.bind="true" ce-id="2">Delivered.</case-host>
        </div>`,
        },
        config(),
        `<div> ${wrap('Delivered.')} </div>`,
        [1, 2, ...getActivationSequenceFor('case-host-2')],
        getDeactivationSequenceFor('case-host-2')
      );

      // tag: nonsense example
      yield new TestData(
        '*[case][if=false] leads to unexpected result',
        {
          initialStatus: Status.delivered,
          template: `
        <div switch.bind="status">
          <case-host case="processing" ce-id="1">Processing your order.</case-host>
          <span if.bind="false" case="delivered">Delivered.</span>
        </div>`,
        },
        config(),
        '<div> </div>',
        [1],
        []
      );

      // tag: nonsense example
      yield new TestData(
        '*[if=false][case] leads to unexpected result',
        {
          initialStatus: Status.delivered,
          template: `
        <div switch.bind="status">
          <case-host case="processing" ce-id="1">Processing your order.</case-host>
          <case-host case="delivered" if.bind="false" ce-id="2">Delivered.</case-host>
        </div>`,
        },
        config(),
        '<div> </div>',
        [1, 2],
        []
      );

      // tag: nonsense example
      yield new TestData(
        '*[switch]>*[case][repeat.for] leads to unexpected result',
        {
          initialStatus: Status.delivered,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case.bind="s" repeat.for="s of ['received','dispatched','processing','delivered',]" ce-id="1">\${s}</case-host>
        </template>
        <template switch.bind="status">
          <case-host case.bind="s" repeat.for="s of ['delivered','received','dispatched','processing',]" ce-id="2">\${s}</case-host>
        </template>
      </template>`,
        },
        config(),
        '',
        [1, 2],
        []
      );

      // tag: nonsense example
      yield new TestData(
        '*[switch]>*[case][repeat.for] - static case - leads to unexpected result',
        {
          initialStatus: Status.received,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case="processing" ce-id="1">Processing your order.</case-host>
          <case-host case="received" repeat.for="i of 3" ce-id.bind="2+i">\${i}</case-host>
        </template>
      </template>`,
        },
        config(),
        `${wrap('0')}${wrap('1')}${wrap('2')}`,
        [1, 2, ...getActivationSequenceFor(['case-host-2', 'case-host-3', 'case-host-4'])],
        getDeactivationSequenceFor(['case-host-2', 'case-host-3', 'case-host-4'])
      );

      // yield new TestData(
      //   'supports nested switch',
      //   {
      //     initialStatus: Status.delivered,
      //     template: `
      // <template>
      //   <let day.bind="2"></let>
      //   <template switch.bind="status">
      //     <case-host case="received"   ce-id="1">Order received.</case-host>
      //     <case-host case="dispatched" ce-id="2">On the way.</case-host>
      //     <case-host case="processing" ce-id="3">Processing your order.</case-host>
      //     <case-host case="delivered"  ce-id="4">
      //       <template switch.bind="day">
      //         Expected to be delivered
      //         <case-host case.bind="1" ce-id="5">tomorrow.</case-host>
      //         <case-host case.bind="2" ce-id="6">in 2 days.</case-host>
      //         <case-host default-case  ce-id="7">in few days.</case-host>
      //       </template>
      //     </case-host>
      //   </template>
      // </template>`,
      //     verifyStopCallsAsSet: true,
      //   },
      //   config(),
      //   wrap(` Expected to be delivered ${wrap('in 2 days.')} `),
      //   null,
      //   getDeactivationSequenceFor(['case-host-4', 'case-host-6']),
      //   (ctx) => {
      //     const $switch = ctx.getSwitches()[0];
      //     const $switch2 = ctx.getSwitches(($switch['cases'][3] as Case).view as unknown as Controller)[0];
      //     ctx.assertCalls([
      //       1, 2, 3, 4, ...getActivationSequenceFor('case-host-4'),
      //       `Case-#${$switch2['cases'][0].id}.isMatch()`, `Case-#${$switch2['cases'][1].id}.isMatch()`, ...getActivationSequenceFor('case-host-6')
      //     ]);
      //   }
      // );

      yield new TestData(
        'works with local template',
        {
          initialStatus: Status.delivered,
          template: `
      <template as-custom-element="foo-bar">
        <bindable name="status"></bindable>
        <div switch.bind="status">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <case-host case="delivered"  ce-id="4">Delivered.</case-host>
        </div>
      </template>

      <foo-bar status.bind="status"></foo-bar>
      `,
        },
        config(),
        `<foo-bar> <div> ${wrap('Delivered.')} </div> </foo-bar>`,
        null,
        getDeactivationSequenceFor('case-host-4'),
        (ctx) => {
          const fooBarController = ctx.controller.children[0];
          const $switch = ctx.getSwitches(fooBarController)[0];
          ctx.assertCalls([
            ...new Array(4).fill(0).map((_, i) => `Case-#${$switch['cases'][i].id}.isMatch()`),
            ...getActivationSequenceFor('case-host-4')
          ]);
        }
      );

      yield new TestData(
        'works with au-slot[case]',
        {
          initialStatus: Status.received,
          template: `
      <template as-custom-element="foo-bar">
        <bindable name="status"></bindable>
        <div switch.bind="status">
          <au-slot name="s1" case="received">Order received.</au-slot>
          <au-slot name="s2" case="dispatched">On the way.</au-slot>
          <au-slot name="s3" case="processing">Processing your order.</au-slot>
          <au-slot name="s4" case="delivered">Delivered.</au-slot>
        </div>
      </template>

      <foo-bar status.bind="status">
        <span au-slot="s1">Projection</span>
      </foo-bar>
      `,
        },
        config(),
        '<foo-bar> <div> <span>Projection</span> </div> </foo-bar>',
        null,
        [],
        async (ctx) => {
          const fooBarController = ctx.controller.children[0];
          const $switch = ctx.getSwitches(fooBarController)[0];
          ctx.assertCalls([`Case-#${$switch['cases'][0].id}.isMatch()`]);

          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            '<foo-bar> <div> Delivered. </div> </foo-bar>',
            new Array(4).fill(0).map((_, i) => `Case-#${$switch['cases'][i].id}.isMatch()`),
          );
        }
      );

      yield new TestData(
        'works with case on CE',
        {
          initialStatus: Status.received,
          template: `
      <template>
        <template switch.bind="status">
          <case-host case="received"   ce-id="1">Order received.</case-host>
          <case-host case="dispatched" ce-id="2">On the way.</case-host>
          <case-host case="processing" ce-id="3">Processing your order.</case-host>
          <my-echo case="delivered"    message="Delivered."></my-echo>
        </template>
      </template>`,
          registrations: [MyEcho]
        },
        config(),
        wrap('Order received.'),
        [1, ...getActivationSequenceFor('case-host-1')],
        getDeactivationSequenceFor('my-echo'),
        async (ctx) => {
          const $switch = ctx.getSwitches()[0];
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            '<my-echo>Echoed \'Delivered.\'</my-echo>',
            [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('my-echo')]
          );
        }
      );

      yield new TestData(
        'slot integration - switch wrapped with au-slot',
        {
          initialStatus: Status.received,
          template: `
      <template as-custom-element="foo-bar">
        <au-slot name="s1"></au-slot>
      </template>

      <foo-bar>
        <template au-slot="s1">
          <template switch.bind="status">
            <case-host case="received"   ce-id="1">Order received.</case-host>
            <case-host case="dispatched" ce-id="2">On the way.</case-host>
            <case-host case="processing" ce-id="3">Processing your order.</case-host>
            <case-host case="delivered"  ce-id="4">Delivered.</case-host>
          </template>
        </template>
      </foo-bar>
      `,
        },
        config(),
        `<foo-bar> ${wrap('Order received.')} </foo-bar>`,
        null,
        getDeactivationSequenceFor('case-host-4'),
        async (ctx) => {
          const fooBarController = ctx.controller.children[0];
          const auSlot: AuSlot = (fooBarController.children[0] as Controller<AuSlot>).viewModel;
          const $switch = ctx.getSwitches(auSlot.view as unknown as Controller)[0];
          ctx.assertCalls([`Case-#${$switch['cases'][0].id}.isMatch()`, ...getActivationSequenceFor('case-host-1')]);
          await ctx.assertChange(
            $switch,
            () => { ctx.app.status = Status.delivered; },
            `<foo-bar> ${wrap('Delivered.')} </foo-bar>`,
            [...new Array(4).fill(0).map((_, i) => `Case-#${$switch['cases'][i].id}.isMatch()`), ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-4')],
          );
        }
      );

      yield new TestData(
        '*[switch] native-html-element *[case] works',
        {
          initialStatus: Status.received,
          template: `
      <template>
        <template switch.bind="status">
          <div>
            <div>
              <case-host case="received"   ce-id="1">Order received.</case-host>
              <case-host case="dispatched" ce-id="2">On the way.</case-host>
              <case-host case="processing" ce-id="3">Processing your order.</case-host>
              <case-host case="delivered"  ce-id="4">Delivered.</case-host>
            </div>
          </div>
        </template>
      </template>`,
        },
        config(),
        `<div> <div> ${wrap('Order received.')} </div> </div>`,
        [1, ...getActivationSequenceFor('case-host-1')],
        getDeactivationSequenceFor('case-host-1')
      );

      // tag: not supported
      // yield new TestData(
      //   '*[switch]>CE>*[case] produces some output',
      //   {
      //     initialStatus: Status.dispatched,
      //     template: `
      // <template as-custom-element="foo-bar">
      //   foo bar
      // </template>

      // <template switch.bind="status">
      //   <foo-bar>
      //     <case-host case="dispatched" ce-id="1">On the way.</case-host>
      //     <case-host case="delivered"  ce-id="2">Delivered.</case-host>
      //   </foo-bar>
      // </template>`,
      //   },
      //   config(),
      //   `<foo-bar> ${wrap('On the way.')} foo bar </foo-bar>`,
      //   [1, ...getActivationSequenceFor('case-host-1')],
      //   getDeactivationSequenceFor('case-host-1')
      // );

      // yield new TestData(
      //   '*[switch]>CE>CE>*[case] works',
      //   {
      //     initialStatus: Status.dispatched,
      //     template: `
      // <template as-custom-element="foo-bar">
      //   foo bar
      // </template>
      // <template as-custom-element="fiz-baz">
      //   fiz baz
      // </template>

      // <template switch.bind="status">
      //   <foo-bar>
      //     <fiz-baz>
      //       <case-host case="dispatched" ce-id="1">On the way.</case-host>
      //       <case-host case="delivered"  ce-id="2">Delivered.</case-host>
      //     </fiz-baz>
      //   </foo-bar>
      // </template>`,
      //   },
      //   config(),
      //   `<foo-bar> <fiz-baz> ${wrap('On the way.')} fiz baz </fiz-baz> foo bar </foo-bar>`,
      //   [1, ...getActivationSequenceFor('case-host-1')],
      //   getDeactivationSequenceFor('case-host-1')
      // );

      // yield new TestData(
      //   'works with case binding changed to array and back',
      //   {
      //     initialStatus: Status.received,
      //     template: `
      // <template>
      //   <let s.bind="'received'"></let>
      //   <template switch.bind="status">
      //     <case-host case.bind="s"     ce-id="1">Order received.</case-host>
      //     <case-host case="dispatched" ce-id="2">On the way.</case-host>
      //     <case-host case="processing" ce-id="3">Processing your order.</case-host>
      //     <case-host case="delivered"  ce-id="4">Delivered.</case-host>
      //   </template>
      // </template>`,
      //   },
      //   config(),
      //   wrap('Order received.'),
      //   [1, ...getActivationSequenceFor('case-host-1')],
      //   getDeactivationSequenceFor('case-host-1'),
      //   async (ctx) => {
      //     const $switch = ctx.getSwitches()[0];

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.app.status = Status.delivered; },
      //       wrap('Delivered.'),
      //       [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-4')]
      //     );

      //     const arr = [Status.received, Status.delivered];
      //     const observer = ctx.container.get(IObserverLocator).getArrayObserver(arr);
      //     const addSpy = createSpy(observer, "subscribe", true);
      //     const removeSpy = createSpy(observer, "unsubscribe", true);

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.controller.scope.overrideContext.s = arr; },
      //       wrap('Order received.'),
      //       [1, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-1')]
      //     );

      //     assert.strictEqual(addSpy.calls.length, 1, 'subscribe count');
      //     assert.strictEqual(addSpy.calls[0][0], $switch['cases'][0], 'subscribe arg');

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.app.status = Status.dispatched; },
      //       wrap('On the way.'),
      //       [1, 2, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-2')]
      //     );

      //     const arr2 = [Status.received, Status.dispatched];
      //     const observer2 = ctx.container.get(IObserverLocator).getArrayObserver(arr2);
      //     const addSpy2 = createSpy(observer2, "subscribe", true);
      //     const removeSpy2 = createSpy(observer2, "unsubscribe", true);

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.controller.scope.overrideContext.s = arr2; },
      //       wrap('Order received.'),
      //       [1, ...getDeactivationSequenceFor('case-host-2'), ...getActivationSequenceFor('case-host-1')]
      //     );
      //     assert.strictEqual(removeSpy.calls.length, 1, 'subscibe count');
      //     assert.strictEqual(removeSpy.calls[0][0], $switch['cases'][0], 'subscibe arg');
      //     assert.strictEqual(addSpy2.calls.length, 1, 'subscibe count #2');
      //     assert.strictEqual(addSpy2.calls[0][0], $switch['cases'][0], 'subscibe arg #2');

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.app.status = Status.delivered; },
      //       wrap('Delivered.'),
      //       [1, 2, 3, 4, ...getDeactivationSequenceFor('case-host-1'), ...getActivationSequenceFor('case-host-4')]
      //     );

      //     await ctx.assertChange(
      //       $switch,
      //       () => { ctx.controller.scope.overrideContext.s = Status.delivered; },
      //       wrap('Order received.'),
      //       [1, ...getDeactivationSequenceFor('case-host-4'), ...getActivationSequenceFor('case-host-1')]
      //     );
      //     assert.strictEqual(removeSpy2.calls.length, 1, 'subscibe count #2');
      //     assert.strictEqual(removeSpy2.calls[0][0], $switch['cases'][0], 'subscibe arg #2');
      //   }
      // );
    }
  }

  for (const data of getTestData()) {
    (data.only ? $it.only : $it)(data.name,
      async function (ctx) {

        assert.strictEqual(ctx.error, null);
        assert.html.innerEqual(ctx.host, data.expectedInnerHtml, 'innerHTML');

        if (data.expectedStartLog !== null) {
          ctx.assertCalls(data.expectedStartLog, 'start lifecycle calls');
        }

        const additionalAssertions = data.additionalAssertions;
        if (additionalAssertions !== null) {
          await additionalAssertions(ctx);
        }
      },
      data);
  }

  function* getNegativeTestData() {
    yield new TestData(
      'case without switch',
      {
        template: `
        <template as-custom-element="foo-bar">
          <case-host case="delivered">delivered</case-host>
        </template>
        <foo-bar></foo-bar>
        `,
      },
      new Config(false, false, noop),
      null!,
      null,
      null
    );
    yield new TestData(
      '*[switch]>*[if]>*[case]',
      {
        template: `
      <template>
        <template switch.bind="status">
          <template if.bind="true">
            <case-host case="delivered">delivered</case-host>
          </template>
        </template>
      </template>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null,
    );

    yield new TestData(
      '*[switch]>*[repeat.for]>*[case]',
      {
        template: `
      <template>
        <template switch.bind="status">
          <template repeat.for="s of ['received','dispatched','processing','delivered',]">
            <case-host case.bind="s">\${s}</case-host>
          </template>
        </template>
      </template>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null,
    );

    yield new TestData(
      '*[switch]>*[repeat.for][case]',
      {
        template: `
      <template>
        <template switch.bind="status">
          <span repeat.for="s of ['received','dispatched','processing','delivered',]" case.bind="s">\${s}</span>
        </template>
      </template>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null,
    );

    yield new TestData(
      '*[switch]>*[au-slot]>*[case]',
      {
        template: `
      <template as-custom-element="foo-bar">
        <au-slot name="s1"></au-slot>
      </template>

      <foo-bar switch.bind="status">
        <template au-slot="s1">
          <case-host case="dispatched">On the way.</case-host>
          <case-host case="delivered">Delivered.</case-host>
        </template>
      </foo-bar>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null,
    );

    yield new TestData(
      '*[if=true][case]',
      {
        template: `
        <div switch.bind="status">
          <case-host case="processing">Processing your order.</case-host>
          <span if.bind="true" case="delivered">Delivered.</span>
        </div>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null
    );

    yield new TestData(
      '*[else][case]',
      {
        initialStatus: Status.delivered,
        template: `
        <div switch.bind="status">
          <span if.bind="false" case="processing">Processing your order.</span>
          <span else case="delivered">Delivered.</span>
        </div>`,
      },
      new Config(false, false, noop),
      null!,
      null,
      null
    );
  }
  for (const data of getNegativeTestData()) {
    $it(`${data.name} does not work`,
      function (ctx) {
        // assert.match(ctx.error.message, /The parent switch not found; only `\*\[switch\] > \*\[case\|default-case\]` relation is supported\./);
        assert.match(ctx.error.message, /AUR0815/);
      },
      data);
  }

  $it(`multiple default-cases throws error`,
    function (ctx) {
      // assert.match(ctx.error.message, /Multiple 'default-case's are not allowed./);
      assert.match(ctx.error.message, /AUR0816/);
    },
    {
      template: `
  <template>
    <template switch.bind="status">
      <case-host case.bind="statuses">Processing.</case-host>
      <case-host case="dispatched">On the way.</case-host>
      <default-case-host default-case>dc1.</default-case-host>
      <default-case-host default-case>dc2.</default-case-host>
    </template>
  </template>`
    });

  $it(`*[case][else] throws error`,
    function (ctx) {
      /**
       * ATM the error is thrown from Else#link as controller.children is undefined.
       * But probably it is not necessary to assert that exact error here.
       */
      assert.match(ctx.error.message, /.+/);
    },
    {
      initialStatus: Status.delivered,
      template: `
        <div switch.bind="status">
          <span if.bind="false" case="processing">Processing your order.</span>
          <case-host case="delivered" else>Delivered.</case-host>
        </div>`
    }
  );

  // eslint-disable-next-line mocha/no-skipped-tests
  it.skip('TODO: supports nested switches', async function () {
    // this already working, just need proper tests
    // the old tests are absolute beast in terms of modifiability + debuggability
    // will need to simplify them using LifeycleHooks or something similar
    await createFixture
      .html`
        <let day.bind="2"></let>
        <template switch.bind="status">
          <div case="received"   ce-id="1">Order received.<div>
          <div case="dispatched" ce-id="2">On the way.<div>
          <div case="processing" ce-id="3">Processing your order.<div>
          <div case="delivered"  ce-id="4" switch.bind="day">
            Expected to be delivered
            <div case.bind="1" ce-id="5">tomorrow.<div>
            <div case.bind="2" ce-id="6">in 2 days.<div>
            <div default-case  ce-id="7">in few days.<div>
          <div>
        </template>
      `
      .build().started;
  });
});