UNPKG

@casual-simulation/aux-common

Version:
2,653 lines 107 kB
/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences.
 *
 * Copyright (c) 2019-2025 Casual Simulation, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import { createBot, objectsAtDimensionGridPosition, getBotShape, calculateBotValue, filterBotsBySelection, isSimulation, isDestroyable, isEditable, duplicateBot, isBotMovable, getUserMenuId, getBotsInMenu, addBotToMenu, removeBotFromMenu, getDimensionVisualizeMode, getBuilderDimensionGrid, getDimensionSize, addToDimensionDiff, removeFromDimensionDiff, isDimensionMovable, isDimension, getBotConfigDimensions, isDimensionLocked, getBotLabelAnchor, getBotVersion, isBotInDimension, getUserBotColor, calculateStringListTagValue, getChannelBotById, calculateBooleanTagValue, calculateNumericalTagValue, getBotScale, isUserActive, calculateStringTagValue, isMinimized, getDimensionColor, getDimensionGridScale, getDimensionScale, getDimensionDefaultHeight, getBotPosition, getBotRotation, botDimensionSortOrder, getPortalConfigBotID, getBotSubShape, getBotOrientationMode, getBotAnchorPoint, getAnchorPointOffset, isBotPointable, isBotFocusable, getBotLabelAlignment, getBotScaleMode, getBotMeetPortalAnchorPoint, getBotMeetPortalAnchorPointOffset, calculateBotIdTagValue, getBotTagPortalAnchorPoint, getBotTagPortalAnchorPointOffset, createPrecalculatedBot, calculateLabelFontSize, calculateLabelWordWrapMode, getMenuBotForm, calculatePortalCameraControlsMode, getMenuBotHoverStyle, getCameraType, getBotCursor, getPortalCursor, getBotLabelPadding, getCursorCSS, calculateGridScale, calculateBotVectorTagValue, calculateBotRotationTagValue, getTagPosition, getTagRotation, formatBotVector, getSystemPortalPane, getOpenSystemPortalPane, getBotTheme, getMenuBotSubtype, getBotMeshPositioningMode, } from '../BotCalculations';
import { DEFAULT_BUILDER_USER_COLOR, DEFAULT_PLAYER_USER_COLOR, DEFAULT_WORKSPACE_SCALE, SYSTEM_PORTAL, SHEET_PORTAL, SYSTEM_PORTAL_DIFF, SYSTEM_PORTAL_SEARCH, SYSTEM_PORTAL_PANE, } from '../Bot';
import { buildLookupTable } from '../BotLookupTable';
import { BotLookupTableHelper } from '../BotLookupTableHelper';
import { stringTagValueTests, booleanTagValueTests, numericalTagValueTests, vectorTagValueTests, rotationTagValueTests, } from './BotTestHelpers';
import { reject, botRemoved, toast } from '../BotEvents';
import { calculateDestroyBotEvents, resolveRejectedActions, } from '../BotActions';
import { Quaternion, Rotation } from '../../math';
export function botCalculationContextTests(uuidMock, dateNowMock, createPrecalculatedContext) {
    describe('objectsAtContextGridPosition()', () => {
        it('should return bots at the given position', () => {
            const bot1 = createBot('test1', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const bot2 = createBot('test2', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const bot3 = createBot('test3', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const context = createPrecalculatedContext([bot2, bot1, bot3]);
            const result = objectsAtDimensionGridPosition(context, 'dimension', {
                x: -1,
                y: 1,
            });
            expect(result).toEqual([bot1, bot2, bot3]);
        });
        it('should cache the query and results', () => {
            const bot1 = createBot('test1', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const bot2 = createBot('test2', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const bot3 = createBot('test3', {
                dimension: true,
                dimensionX: -1,
                dimensionY: 1,
            });
            const context = createPrecalculatedContext([bot2, bot1, bot3]);
            const context2 = createPrecalculatedContext([bot2, bot1, bot3]);
            const result1 = objectsAtDimensionGridPosition(context, 'dimension', {
                x: -1,
                y: 1,
            });
            const result2 = objectsAtDimensionGridPosition(context, 'dimension', {
                x: -1,
                y: 1,
            });
            const result3 = objectsAtDimensionGridPosition(context2, 'dimension', {
                x: -1,
                y: 1,
            });
            expect(result1).toBe(result2);
            expect(result1).not.toBe(result3);
        });
        it('should default to 0,0 for bots without a position', () => {
            const bot1 = createBot('test1', {
                dimension: true,
            });
            const context = createPrecalculatedContext([bot1]);
            const result = objectsAtDimensionGridPosition(context, 'dimension', {
                x: 0,
                y: 0,
            });
            expect(result).toEqual([bot1]);
        });
    });
    describe('calculateGridScale()', () => {
        it('should return the default grid scale', () => {
            const bot = createBot('test');
            const context = createPrecalculatedContext([bot]);
            const result = calculateGridScale(context, bot);
            expect(result).toBe(0.4);
        });
        it('should portalSurfaceScale', () => {
            const bot = createBot('test', {
                portalSurfaceScale: 4,
            });
            const context = createPrecalculatedContext([bot]);
            const result = calculateGridScale(context, bot);
            expect(result).toBe(0.8);
        });
        it('should support portalGridScale', () => {
            const bot = createBot('test', {
                portalGridScale: 1,
            });
            const context = createPrecalculatedContext([bot]);
            const result = calculateGridScale(context, bot);
            expect(result).toBe(2);
        });
        it('should support both portalSurfaceScale and portalGridScale', () => {
            const bot = createBot('test', {
                portalGridScale: 1,
                portalSurfaceScale: 4,
            });
            const context = createPrecalculatedContext([bot]);
            const result = calculateGridScale(context, bot);
            expect(result).toBe(4);
        });
        it('should support custom defaults', () => {
            const bot = createBot('test', {});
            const context = createPrecalculatedContext([bot]);
            const result = calculateGridScale(context, bot, 1, 4);
            expect(result).toBe(4);
        });
        it('should support null bots', () => {
            const context = createPrecalculatedContext([]);
            const result = calculateGridScale(context, null, 1, 4);
            expect(result).toBe(4);
        });
    });
    describe('calculateBotValue()', () => {
        it('should convert to a number if it is a number', () => {
            const bot = createBot();
            bot.tags.tag = '123.145';
            const context = createPrecalculatedContext([bot]);
            const value = calculateBotValue(context, bot, 'tag');
            expect(value).toBeCloseTo(123.145);
        });
        it('should parse numbers that dont start with a digit', () => {
            const bot = createBot();
            bot.tags.tag = '.145';
            const context = createPrecalculatedContext([bot]);
            const value = calculateBotValue(context, bot, 'tag');
            expect(value).toBeCloseTo(0.145);
        });
        it('should convert to a boolean if it is a boolean', () => {
            const bot = createBot();
            bot.tags.tag = 'true';
            const context = createPrecalculatedContext([bot]);
            const trueValue = calculateBotValue(context, bot, 'tag');
            expect(trueValue).toBe(true);
            bot.tags.tag = 'false';
            const falseValue = calculateBotValue(context, bot, 'tag');
            expect(falseValue).toBe(false);
        });
        it('should keep arrays as strings', () => {
            const bot = createBot();
            bot.tags.tag = '[test(a, b, c), 1.23, true]';
            const context = createPrecalculatedContext([bot]);
            const value = calculateBotValue(context, bot, 'tag');
            expect(value).toEqual('[test(a, b, c), 1.23, true]');
        });
        it('should return the bot ID for the id tag', () => {
            const bot = createBot('test', {});
            const context = createPrecalculatedContext([bot]);
            const value = calculateBotValue(context, bot, 'id');
            expect(value).toEqual('test');
        });
        describe('space', () => {
            it('should return shared if the space is not defined', () => {
                const bot = createBot('test', {});
                const context = createPrecalculatedContext([bot]);
                const value = calculateBotValue(context, bot, 'space');
                expect(value).toEqual('shared');
            });
            it('should return the space if it is defined', () => {
                const bot = createBot('test', {}, 'local');
                const context = createPrecalculatedContext([bot]);
                const value = calculateBotValue(context, bot, 'space');
                expect(value).toEqual('local');
            });
        });
    });
    describe('filterBotsBySelection()', () => {
        it('should return the bots that have the given selection ID set in a tag', () => {
            const selectionId = 'abcdefg1234';
            const bot1 = createBot('test1');
            const bot2 = createBot('test2');
            const bot3 = createBot('test3');
            const bot4 = createBot('test4');
            const bot5 = createBot('test5');
            const bot6 = createBot('test6');
            const bot7 = createBot('test7');
            bot1.tags[selectionId] = true;
            bot2.tags[selectionId] = 1;
            bot3.tags[selectionId] = -1;
            bot4.tags[selectionId] = 'hello';
            bot5.tags[selectionId] = false;
            bot6.tags[selectionId] = '';
            const selected = filterBotsBySelection([bot1, bot2, bot3, bot4, bot5, bot6, bot7], selectionId);
            expect(selected).toEqual([bot1, bot2, bot3, bot4, bot5]);
        });
        it('should return bots that have the same ID as the selection', () => {
            const selectionId = 'abcdefg1234';
            const bot1 = createBot('test1');
            const bot2 = createBot('abcdefg1234');
            bot1.tags[selectionId] = true;
            const selected = filterBotsBySelection([bot1, bot2], selectionId);
            expect(selected).toEqual([bot1, bot2]);
        });
        it('should support the id tag', () => {
            const selectionId = 'id';
            const bot1 = createBot('test1');
            const bot2 = createBot('abcdefg1234');
            bot1.tags[selectionId] = true;
            const selected = filterBotsBySelection([bot1, bot2], selectionId);
            expect(selected).toEqual([bot1, bot2]);
        });
        it('should support the space tag', () => {
            const selectionId = 'space';
            const bot1 = createBot('test1');
            const bot2 = createBot('abcdefg1234');
            bot1.tags[selectionId] = true;
            const selected = filterBotsBySelection([bot1, bot2], selectionId);
            expect(selected).toEqual([bot1, bot2]);
        });
        it('should support tag masks', () => {
            const selectionId = 'abc';
            const bot1 = createPrecalculatedBot('test1', {}, {});
            const bot2 = createPrecalculatedBot('abcdefg1234', {}, {});
            bot1.values[selectionId] = true;
            bot1.masks = {
                tempLocal: {
                    [selectionId]: true,
                },
            };
            const selected = filterBotsBySelection([bot1, bot2], selectionId);
            expect(selected).toEqual([bot1]);
        });
        it('should include bots that have a tag but no value', () => {
            const selectionId = 'abc';
            const bot1 = createPrecalculatedBot('test1', {
                [selectionId]: null,
            }, {
                [selectionId]: 'abc',
            });
            const bot2 = createPrecalculatedBot('abcdefg1234', {}, {});
            const selected = filterBotsBySelection([bot1, bot2], selectionId);
            expect(selected).toEqual([bot1]);
        });
    });
    describe('calculateBooleanTagValue()', () => {
        booleanTagValueTests(false, (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBooleanTagValue(calc, bot, 'tag', false)).toBe(expected);
        });
        it('should support fallback from aux prefixed tags', () => {
            let bot = createBot('test', {
                tag: true,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBooleanTagValue(calc, bot, 'auxTag', false)).toBe(true);
        });
    });
    describe('calculateStringTagValue()', () => {
        stringTagValueTests('test', (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateStringTagValue(calc, bot, 'tag', 'test')).toBe(expected);
        });
        it('should support fallback from aux prefixed tags', () => {
            let bot = createBot('test', {
                tag: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateStringTagValue(calc, bot, 'auxTag', 'empty')).toBe('abc');
        });
    });
    describe('calculateBotIdTagValue()', () => {
        stringTagValueTests('test', (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotIdTagValue(bot, 'tag', 'test')).toBe(expected);
        });
        it('should return the ID of the bot', () => {
            let bot = createBot('test', {
                tag: createBot('botId'),
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotIdTagValue(bot, 'tag', 'empty')).toBe('botId');
        });
        it('should return the default if the object is not a bot', () => {
            let bot = createBot('test', {
                tag: {},
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotIdTagValue(bot, 'tag', 'empty')).toBe('empty');
        });
        it('should support fallback from aux prefixed tags', () => {
            let bot = createBot('test', {
                tag: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotIdTagValue(bot, 'auxTag', 'empty')).toBe('abc');
        });
        it('should support bot links', () => {
            let bot = createBot('test', {
                tag: '🔗abc',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotIdTagValue(bot, 'tag', 'empty')).toBe('abc');
        });
    });
    describe('calculateNumericalTagValue()', () => {
        numericalTagValueTests(null, (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateNumericalTagValue(calc, bot, 'tag', null)).toBe(expected);
        });
        it('should support fallback from aux prefixed tags', () => {
            let bot = createBot('test', {
                tag: 123,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateNumericalTagValue(calc, bot, 'auxTag', 0)).toBe(123);
        });
    });
    describe('calculateBotVectorTagValue()', () => {
        vectorTagValueTests(null, (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotVectorTagValue(calc, bot, 'tag', null)).toEqual(expected);
        });
    });
    describe('calculateBotRotationTagValue()', () => {
        rotationTagValueTests(null, (value, expected) => {
            let bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(calculateBotRotationTagValue(calc, bot, 'tag', null)).toEqual(expected);
        });
    });
    describe('isUserActive()', () => {
        const tags = ['auxPlayerActive', 'playerActive'];
        describe.each(tags)('%s', (tag) => {
            it(`should return true if the ${tag} tag is true`, () => {
                dateNowMock.mockReturnValue(1000 * 60 + 999);
                const bot1 = createBot(undefined, {
                    [tag]: true,
                });
                const calc = createPrecalculatedContext([bot1]);
                const update1 = isUserActive(calc, bot1);
                expect(update1).toBe(true);
            });
            it('should return false if the user is not active', () => {
                dateNowMock.mockReturnValue(1000);
                const bot1 = createBot(undefined, {
                    [tag]: false,
                });
                const calc = createPrecalculatedContext([bot1]);
                const update1 = isUserActive(calc, bot1);
                expect(update1).toBe(false);
            });
        });
    });
    describe('isSimulation()', () => {
        let cases = [
            ['', false],
            [null, false],
            [0, false],
            ['false', false],
            ['0', false],
            ['a', true],
            [1, true],
            [true, true],
            ['=1', true],
            ['="hello"', true],
        ];
        it.each(cases)('should map server:%s to %s', (value, expected) => {
            let bot = createBot('test', {
                server: value,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(isSimulation(calc, bot)).toBe(expected);
        });
    });
    describe('isDestroyable()', () => {
        describe('auxDestroyable', () => {
            booleanTagValueTests(true, (value, expected) => {
                let bot = createBot('test', {
                    auxDestroyable: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(isDestroyable(calc, bot)).toBe(expected);
            });
        });
        describe('destroyable', () => {
            booleanTagValueTests(true, (value, expected) => {
                let bot = createBot('test', {
                    destroyable: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(isDestroyable(calc, bot)).toBe(expected);
            });
        });
    });
    describe('isEditable()', () => {
        describe('auxEditable', () => {
            booleanTagValueTests(true, (value, expected) => {
                let bot = createBot('test', {
                    auxEditable: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(isEditable(calc, bot)).toBe(expected);
            });
        });
        describe('editable', () => {
            booleanTagValueTests(true, (value, expected) => {
                let bot = createBot('test', {
                    editable: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(isEditable(calc, bot)).toBe(expected);
            });
        });
    });
    describe('isMinimized()', () => {
        it('should return true when auxPortalSurfaceMinimized is true', () => {
            let bot = createBot('test', {
                auxPortalSurfaceMinimized: true,
            });
            const context = createPrecalculatedContext([bot]);
            expect(isMinimized(context, bot)).toBe(true);
        });
        it('should return false when auxPortalSurfaceMinimized is not true', () => {
            let bot = createBot('test', {
                auxPortalSurfaceMinimized: false,
            });
            const context = createPrecalculatedContext([bot]);
            expect(isMinimized(context, bot)).toBe(false);
        });
    });
    describe('duplicateBot()', () => {
        beforeAll(() => {
            uuidMock.mockReturnValue('test');
        });
        it('should return a copy with a different ID', () => {
            const first = createBot('id');
            first.tags.fun = 'abc';
            const calc = createPrecalculatedContext([first]);
            const second = duplicateBot(calc, first);
            expect(second.id).not.toEqual(first.id);
            expect(second.id).toBe('test');
            expect(second.tags).toEqual(first.tags);
        });
        it('should not have any auto-generated contexts or selections', () => {
            let first = createBot('id');
            first.tags[`aux.other`] = 100;
            first.tags[`myTag`] = 'Hello';
            first.tags[`aux._context_abcdefg`] = true;
            first.tags[`aux._context_1234567`] = true;
            first.tags[`aux._context_1234567X`] = 1;
            first.tags[`aux._context_1234567Y`] = 2;
            first.tags[`aux._context_1234567Z`] = 3;
            first.tags[`_auxSelection99999`] = true;
            const calc = createPrecalculatedContext([first]);
            const second = duplicateBot(calc, first);
            expect(second.id).not.toEqual(first.id);
            expect(second.tags).toEqual({
                'aux.other': 100,
                myTag: 'Hello',
            });
            expect(first.tags).toEqual({
                'aux.other': 100,
                myTag: 'Hello',
                'aux._context_abcdefg': true,
                'aux._context_1234567': true,
                'aux._context_1234567X': 1,
                'aux._context_1234567Y': 2,
                'aux._context_1234567Z': 3,
                _auxSelection99999: true,
            });
        });
        it('should keep the tags that the new data contains', () => {
            let first = createBot('id');
            first.tags[`aux.other`] = 100;
            first.tags[`myTag`] = 'Hello';
            const calc = createPrecalculatedContext([first]);
            const second = duplicateBot(calc, first, {
                tags: {
                    [`_auxSelection99999`]: true,
                    [`aux._context_abcdefg`]: true,
                },
            });
            expect(second.id).not.toEqual(first.id);
            expect(second.tags).toEqual({
                'aux.other': 100,
                myTag: 'Hello',
                'aux._context_abcdefg': true,
                _auxSelection99999: true,
            });
        });
        it('should merge in the additional changes', () => {
            let first = createBot('id', {
                testTag: 'abcdefg',
                name: 'ken',
            });
            const calc = createPrecalculatedContext([first]);
            const second = duplicateBot(calc, first, {
                tags: {
                    name: 'abcdef',
                },
            });
            expect(second.id).not.toEqual(first.id);
            expect(second.tags).toEqual({
                testTag: 'abcdefg',
                name: 'abcdef',
            });
        });
        it('should not modify the original bot', () => {
            let first = createBot('id');
            first.tags['_auxHidden'] = true;
            const calc = createPrecalculatedContext([first]);
            expect(first.tags['_auxHidden']).toBe(true);
        });
        it('should not have any contexts', () => {
            let first = createBot('id', {
                abc: true,
                abcX: 1,
                abcY: 2,
                def: true,
            });
            let dimension = createBot('dimension', {
                auxDimensionConfig: 'abc',
            });
            const calc = createPrecalculatedContext([dimension, first]);
            const second = duplicateBot(calc, first);
            expect(second.tags).toEqual({
                def: true,
            });
        });
    });
    describe('isBotMovable()', () => {
        it('should return true when draggable has no value', () => {
            let bot = createBot('test', {});
            const context = createPrecalculatedContext([bot]);
            expect(isBotMovable(context, bot)).toBe(true);
        });
        it('should return false when draggable is false', () => {
            let bot = createBot('test', {
                ['draggable']: false,
            });
            const context = createPrecalculatedContext([bot]);
            expect(isBotMovable(context, bot)).toBe(false);
        });
        it('should return true when draggable has any other value', () => {
            let bot = createBot('test', {
                ['draggable']: 'anything',
            });
            const context = createPrecalculatedContext([bot]);
            expect(isBotMovable(context, bot)).toBe(true);
        });
    });
    describe('getBotShape()', () => {
        const cases = [
            ['cube'],
            ['skybox'],
            ['sphere'],
            ['sprite'],
            ['circle'],
            ['mesh'],
            ['iframe'],
            ['nothing'],
            ['frustum'],
            ['egg'],
            ['helix'],
            ['hex'],
            ['cursor'],
            ['portal'],
            ['dimension'],
            ['keyboard'],
            ['codeButton'],
            ['codeHint'],
            ['spherePortal'],
            ['light'],
        ];
        const tagCases = ['auxForm', 'form'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (shape) => {
                const bot = createBot('test', {
                    [tag]: shape,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotShape(calc, bot)).toBe(shape);
            });
            it('should return the shape from the tag', () => {
                let bot = createBot();
                bot.tags[tag] = 'sphere';
                const calc = createPrecalculatedContext([bot]);
                const shape = getBotShape(calc, bot);
                expect(shape).toBe('sphere');
            });
        });
        it('should default to cube', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotShape(calc, bot);
            expect(shape).toBe('cube');
        });
    });
    describe('getMenuBotForm()', () => {
        const cases = [['button'], ['input']];
        const tagCases = ['auxForm', 'form'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (shape) => {
                const bot = createBot('test', {
                    [tag]: shape,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getMenuBotForm(calc, bot)).toBe(shape);
            });
            it('should return the shape from the tag', () => {
                let bot = createBot();
                bot.tags[tag] = 'input';
                const calc = createPrecalculatedContext([bot]);
                const shape = getMenuBotForm(calc, bot);
                expect(shape).toBe('input');
            });
        });
        it('should default to button', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getMenuBotForm(calc, bot);
            expect(shape).toBe('button');
        });
    });
    describe('getMenuBotSubtype()', () => {
        const cases = [['input'], ['password']];
        const tagCases = ['auxFormSubtype', 'formSubtype'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (shape) => {
                const bot = createBot('test', {
                    [tag]: shape,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getMenuBotSubtype(calc, bot)).toBe(shape);
            });
        });
        it('should default to input', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getMenuBotSubtype(calc, bot);
            expect(shape).toBe('input');
        });
    });
    describe('getMenuBotHoverStyle()', () => {
        const cases = [['hover'], ['none']];
        const tagCases = ['auxMenuItemHoverMode', 'menuItemHoverMode'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (shape) => {
                const bot = createBot('test', {
                    [tag]: shape,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getMenuBotHoverStyle(calc, bot)).toBe(shape);
            });
            it('should return the shape from the tag', () => {
                let bot = createBot();
                bot.tags[tag] = 'hover';
                const calc = createPrecalculatedContext([bot]);
                const shape = getMenuBotHoverStyle(calc, bot);
                expect(shape).toBe('hover');
            });
        });
        it('should default to hover if the bot has an onClick', () => {
            const bot = createBot('test', {
                onClick: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            const shape = getMenuBotHoverStyle(calc, bot);
            expect(shape).toBe('hover');
        });
        it('should default to none if the bot does not have an onClick', () => {
            const bot = createBot('test', {});
            const calc = createPrecalculatedContext([bot]);
            const shape = getMenuBotHoverStyle(calc, bot);
            expect(shape).toBe('none');
        });
    });
    describe('getBotSubShape()', () => {
        const cases = [
            ['gltf'],
            ['src'],
            ['html'],
            ['pointLight'],
            ['ambientLight'],
            ['directionalLight'],
            ['spotLight'],
            ['hemisphereLight'],
            ['ldraw'],
            ['ldrawText'],
            ['jsonObject'],
        ];
        const tagCases = ['auxFormSubtype', 'formSubtype'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (shape) => {
                const bot = createBot('test', {
                    [tag]: shape,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotSubShape(calc, bot)).toBe(shape);
            });
        });
        it('should default to null', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotSubShape(calc, bot);
            expect(shape).toBe(null);
        });
    });
    describe('getBotOrientationMode()', () => {
        const cases = [
            ['absolute'],
            ['billboard'],
            ['billboardTop'],
            ['billboardFront'],
        ];
        const tagCases = ['auxOrientationMode', 'orientationMode'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (mode) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotOrientationMode(calc, bot)).toBe(mode);
            });
        });
        it('should default to absolute', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotOrientationMode(calc, bot);
            expect(shape).toBe('absolute');
        });
    });
    describe('getBotAnchorPoint()', () => {
        const cases = [
            ['center', 'center'],
            ['front', 'front'],
            ['back', 'back'],
            ['bottom', 'bottom'],
            ['top', 'top'],
            ['left', 'left'],
            ['right', 'right'],
            [
                [1, 2, 3],
                [1, 2, 3],
            ],
            [formatBotVector({ x: 1, y: 2, z: 3 }), [1, 2, 3]],
            [formatBotVector({ x: 1, y: 2 }), [1, 2, 0]],
        ];
        const tagCases = ['auxAnchorPoint', 'anchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotAnchorPoint(calc, bot)).toEqual(expected);
            });
        });
        it('should default to bottom', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotAnchorPoint(calc, bot);
            expect(shape).toBe('bottom');
        });
    });
    const portalAnchorPointCases = [
        ['fullscreen', 'fullscreen'],
        ['top', 'top'],
        ['topRight', 'topRight'],
        ['topLeft', 'topLeft'],
        ['bottom', 'bottom'],
        ['bottomRight', 'bottomRight'],
        ['bottomLeft', 'bottomLeft'],
        [[1], [1, 0, 0, 0]],
        [
            [1, 2],
            [1, 2, 0, 0],
        ],
        [
            [1, 2, 3],
            [1, 2, 3, 0],
        ],
        [
            [1, 2, 3, 4],
            [1, 2, 3, 4],
        ],
        [
            [1, 2, 3, 4, 5],
            [1, 2, 3, 4],
        ],
        [['a'], ['a', 0, 0, 0]],
        [
            ['a', 'b'],
            ['a', 'b', 0, 0],
        ],
        [
            ['a', 'b', 'c'],
            ['a', 'b', 'c', 0],
        ],
        [
            ['a', 'b', 'c', 'd'],
            ['a', 'b', 'c', 'd'],
        ],
        [
            ['a', 'b', 'c', 'd', 'e'],
            ['a', 'b', 'c', 'd'],
        ],
    ];
    describe('getMeetPortalAnchorPoint()', () => {
        const tagCases = ['auxMeetPortalAnchorPoint', 'meetPortalAnchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(portalAnchorPointCases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotMeetPortalAnchorPoint(calc, bot)).toEqual(expected);
            });
        });
        it('should default to fullscreen', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotMeetPortalAnchorPoint(calc, bot);
            expect(shape).toBe('fullscreen');
        });
    });
    describe('getTagPortalAnchorPoint()', () => {
        const tagCases = ['auxTagPortalAnchorPoint', 'tagPortalAnchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(portalAnchorPointCases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotTagPortalAnchorPoint(calc, bot)).toEqual(expected);
            });
        });
        it('should default to fullscreen', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotTagPortalAnchorPoint(calc, bot);
            expect(shape).toBe('fullscreen');
        });
    });
    describe('getSystemPortalPane()', () => {
        const cases = [['bots'], ['sheet'], ['search'], ['diff']];
        const tagCases = ['systemPortalPane'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (pane) => {
                const bot = createBot('test', {
                    [tag]: pane,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getSystemPortalPane(calc, bot)).toBe(pane);
            });
            it('should return null if given something else', () => {
                let bot = createBot();
                bot.tags[tag] = 'something else';
                const calc = createPrecalculatedContext([bot]);
                const pane = getSystemPortalPane(calc, bot);
                expect(pane).toBe(null);
            });
        });
        it('should default to null', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const pane = getSystemPortalPane(calc, bot);
            expect(pane).toBe(null);
        });
    });
    describe('getOpenSystemPortalPane()', () => {
        it('should return null if the system portal is not open', () => {
            const bot = createBot('test', {});
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe(null);
        });
        it('should return bots if the system portal is set to true', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('bots');
        });
        it('should return sheet if the system portal is set to true and the sheetPortal is open', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SHEET_PORTAL]: 'home',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('sheet');
        });
        it('should return bots if the system portal pane is set to sheet but there is not sheet portal', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_PANE]: 'sheet',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('bots');
        });
        it('should return diff if the system portal is set to true and the systemPortalDiff is set', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_DIFF]: 'home',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('diff');
        });
        it('should return search if the system portal is set to true and there is a search value', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_SEARCH]: 'my search',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('search');
        });
        it('should return prioritize search over the sheet', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SHEET_PORTAL]: 'home',
                [SYSTEM_PORTAL_SEARCH]: 'my search',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('search');
        });
        it('should return prioritize search over the diff', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_DIFF]: 'home',
                [SYSTEM_PORTAL_SEARCH]: 'my search',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('search');
        });
        it('should prioritize the specified pane over search', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_PANE]: 'bots',
                [SYSTEM_PORTAL_SEARCH]: 'my search',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('bots');
        });
        it('should prioritize the specified pane over the diff', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_PANE]: 'bots',
                [SYSTEM_PORTAL_DIFF]: 'my diff',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('bots');
        });
        it('should prioritize the specified pane over the sheet', () => {
            const bot = createBot('test', {
                [SYSTEM_PORTAL]: true,
                [SYSTEM_PORTAL_PANE]: 'bots',
                [SHEET_PORTAL]: 'home',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getOpenSystemPortalPane(calc, bot)).toBe('bots');
        });
    });
    describe('getCameraType()', () => {
        const tagCases = ['auxPortalCameraType', 'portalCameraType'];
        const cameraTypeCases = [
            ['orthographic', 'orthographic'],
            ['perspective', 'perspective'],
            ['other', null],
        ];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cameraTypeCases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getCameraType(calc, bot)).toEqual(expected);
            });
        });
        it('should default to fullscreen', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = getBotTagPortalAnchorPoint(calc, bot);
            expect(shape).toBe('fullscreen');
        });
    });
    describe('getAnchorPointOffset()', () => {
        const cases = [
            ['center', { x: 0, y: 0, z: 0 }],
            ['front', { x: 0, y: -0.5, z: 0 }],
            ['back', { x: 0, y: 0.5, z: 0 }],
            ['bottom', { x: 0, y: 0, z: 0.5 }],
            ['top', { x: 0, y: 0, z: -0.5 }],
            ['left', { x: 0.5, y: 0, z: 0 }],
            ['right', { x: -0.5, y: 0, z: 0 }],
            // Should mirror the coordinates when using literals
            [[1, 2, 3], { x: -1, y: -2, z: -3 }],
        ];
        const tagCases = ['auxAnchorPoint', 'anchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getAnchorPointOffset(calc, bot)).toEqual(expected);
            });
        });
        it('should default to bottom', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const offset = getAnchorPointOffset(calc, bot);
            expect(offset).toEqual({
                x: 0,
                y: 0,
                z: 0.5,
            });
        });
    });
    const portalAnchorPointOffsetCases = [
        [
            'fullscreen',
            { top: '0px', bottom: '0px', left: '0px', right: '0px' },
        ],
        [[1, 2, 3], { top: '1px', bottom: '3px', left: '0px', right: '2px' }],
        [
            ['1%', '2%', '3%'],
            { top: '1%', bottom: '3%', left: '0px', right: '2%' },
        ],
        [
            'top',
            {
                top: '0px',
                height: '50%',
                'min-height': '250px',
                left: '0px',
                right: '0px',
            },
        ],
        [
            'topRight',
            {
                top: '25px',
                height: '25%',
                'min-height': '250px',
                width: '25%',
                'min-width': '250px',
                right: '25px',
            },
        ],
        [
            'topLeft',
            {
                top: '25px',
                height: '25%',
                'min-height': '250px',
                width: '25%',
                'min-width': '250px',
                left: '25px',
            },
        ],
        [
            'bottom',
            {
                bottom: '0px',
                height: '50%',
                'min-height': '250px',
                left: '0px',
                right: '0px',
            },
        ],
        [
            'bottomRight',
            {
                bottom: '25px',
                height: '25%',
                'min-height': '250px',
                width: '25%',
                'min-width': '250px',
                right: '25px',
            },
        ],
        [
            'bottomLeft',
            {
                bottom: '25px',
                height: '25%',
                'min-height': '250px',
                width: '25%',
                'min-width': '250px',
                left: '25px',
            },
        ],
        [
            'left',
            {
                bottom: '0px',
                height: '100%',
                'min-height': '250px',
                width: '50%',
                'min-width': '250px',
                left: '0px',
            },
        ],
        [
            'right',
            {
                bottom: '0px',
                height: '100%',
                'min-height': '250px',
                width: '50%',
                'min-width': '250px',
                right: '0px',
            },
        ],
    ];
    describe('getBotMeetPortalAnchorPointOffset()', () => {
        const tagCases = ['auxMeetPortalAnchorPoint', 'meetPortalAnchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(portalAnchorPointOffsetCases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotMeetPortalAnchorPointOffset(calc, bot)).toEqual(expected);
            });
        });
        it('should default to fullscreen', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const offset = getBotMeetPortalAnchorPointOffset(calc, bot);
            expect(offset).toEqual({
                bottom: '0px',
                left: '0px',
                right: '0px',
                top: '0px',
            });
        });
    });
    const cursorCases = [
        ['auto', 'auto'],
        ['default', 'default'],
        ['none', 'none'],
        ['context-menu', 'context-menu'],
        ['help', 'help'],
        ['pointer', 'pointer'],
        ['progress', 'progress'],
        ['wait', 'wait'],
        ['cell', 'cell'],
        ['crosshair', 'crosshair'],
        ['text', 'text'],
        ['vertical-text', 'vertical-text'],
        ['alias', 'alias'],
        ['copy', 'copy'],
        ['move', 'move'],
        ['no-drop', 'no-drop'],
        ['not-allowed', 'not-allowed'],
        ['grab', 'grab'],
        ['grabbing', 'grabbing'],
        ['all-scroll', 'all-scroll'],
        ['col-resize', 'col-resize'],
        ['row-resize', 'row-resize'],
        ['n-resize', 'n-resize'],
        ['e-resize', 'e-resize'],
        ['s-resize', 's-resize'],
        ['w-resize', 'w-resize'],
        ['ne-resize', 'ne-resize'],
        ['nw-resize', 'nw-resize'],
        ['se-resize', 'se-resize'],
        ['sw-resize', 'sw-resize'],
        ['ew-resize', 'ew-resize'],
        ['ns-resize', 'ns-resize'],
        ['nesw-resize', 'nesw-resize'],
        ['nwse-resize', 'nwse-resize'],
        ['zoom-in', 'zoom-in'],
        ['zoom-out', 'zoom-out'],
        ['missing', 'auto'],
        ['', null],
        [123, null],
        [true, null],
        [
            'http://example.com',
            { type: 'link', url: 'http://example.com', x: 0, y: 0 },
        ],
    ];
    describe('getBotCursor()', () => {
        const tagCases = ['auxCursor', 'cursor'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cursorCases)('should support %s', (value, expected) => {
                const bot = createBot('test', {
                    [tag]: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotCursor(calc, bot)).toEqual(expected);
            });
            it('should support X and Y offsets for cursors', () => {
                const bot = createBot('test', {
                    [tag]: 'http://example.com',
                    cursorHotspotX: 5,
                    cursorHotspotY: 1,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotCursor(calc, bot)).toEqual({
                    type: 'link',
                    url: 'http://example.com',
                    x: 5,
                    y: 1,
                });
            });
            it('should support bot vectors for hotspots', () => {
                const bot = createBot('test', {
                    [tag]: 'http://example.com',
                    cursorHotspot: formatBotVector({ x: 1, y: 2, z: 3 }),
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotCursor(calc, bot)).toEqual({
                    type: 'link',
                    url: 'http://example.com',
                    x: 1,
                    y: 2,
                });
            });
        });
    });
    describe('getPortalCursor()', () => {
        const tagCases = ['auxPortalCursor', 'portalCursor'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cursorCases)('should support %s', (value, expected) => {
                const bot = createBot('test', {
                    [tag]: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getPortalCursor(calc, bot)).toEqual(expected);
            });
            it('should support X and Y offsets for cursors', () => {
                const bot = createBot('test', {
                    [tag]: 'http://example.com',
                    portalCursorHotspotX: 5,
                    portalCursorHotspotY: 1,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getPortalCursor(calc, bot)).toEqual({
                    type: 'link',
                    url: 'http://example.com',
                    x: 5,
                    y: 1,
                });
            });
            it('should support bot vectors for hotspots', () => {
                const bot = createBot('test', {
                    [tag]: 'http://example.com',
                    portalCursorHotspot: formatBotVector({ x: 1, y: 2, z: 3 }),
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getPortalCursor(calc, bot)).toEqual({
                    type: 'link',
                    url: 'http://example.com',
                    x: 1,
                    y: 2,
                });
            });
        });
    });
    describe('getCursorCSS()', () => {
        const cursorCases = [
            ['auto', 'auto'],
            ['default', 'default'],
            ['none', 'none'],
            ['context-menu', 'context-menu'],
            ['help', 'help'],
            ['pointer', 'pointer'],
            ['progress', 'progress'],
            ['wait', 'wait'],
            ['cell', 'cell'],
            ['crosshair', 'crosshair'],
            ['text', 'text'],
            ['vertical-text', 'vertical-text'],
            ['alias', 'alias'],
            ['copy', 'copy'],
            ['move', 'move'],
            ['no-drop', 'no-drop'],
            ['not-allowed', 'not-allowed'],
            ['grab', 'grab'],
            ['grabbing', 'grabbing'],
            ['all-scroll', 'all-scroll'],
            ['col-resize', 'col-resize'],
            ['row-resize', 'row-resize'],
            ['n-resize', 'n-resize'],
            ['e-resize', 'e-resize'],
            ['s-resize', 's-resize'],
            ['w-resize', 'w-resize'],
            ['ne-resize', 'ne-resize'],
            ['nw-resize', 'nw-resize'],
            ['se-resize', 'se-resize'],
            ['sw-resize', 'sw-resize'],
            ['ew-resize', 'ew-resize'],
            ['ns-resize', 'ns-resize'],
            ['nesw-resize', 'nesw-resize'],
            ['nwse-resize', 'nwse-resize'],
            ['zoom-in', 'zoom-in'],
            ['zoom-out', 'zoom-out'],
            [
                { type: 'link', url: 'myUrl', x: 0, y: 0 },
                'url("myUrl") 0 0, auto',
            ],
        ];
        it.each(cursorCases)('should support %s', (value, expected) => {
            expect(getCursorCSS(value)).toEqual(expected);
        });
    });
    describe('getTagPortalAnchorPointOffset()', () => {
        const tagCases = ['auxTagPortalAnchorPoint', 'tagPortalAnchorPoint'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(portalAnchorPointOffsetCases)('should support %s', (mode, expected) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotTagPortalAnchorPointOffset(calc, bot)).toEqual(expected);
            });
        });
        it('should default to fullscreen', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const offset = getBotTagPortalAnchorPointOffset(calc, bot);
            expect(offset).toEqual({
                bottom: '0px',
                left: '0px',
                right: '0px',
                top: '0px',
            });
        });
    });
    describe('calculatePortalCameraControlsMode()', () => {
        const cases = [['player'], [false]];
        const tagCases = ['auxPortalCameraControls', 'portalCameraControls'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (mode) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(calculatePortalCameraControlsMode(calc, bot)).toBe(mode);
            });
        });
        it('should default to player', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = calculatePortalCameraControlsMode(calc, bot);
            expect(shape).toBe('player');
        });
    });
    describe('calculateLabelFontSize()', () => {
        const cases = [['auto'], [10], [1]];
        const tagCases = ['auxLabelFontSize', 'labelFontSize'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (mode) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(calculateLabelFontSize(calc, bot)).toBe(mode);
            });
            it('should return 0.001 for 0', () => {
                const bot = createBot('test', {
                    [tag]: 0,
                });
                const calc = createPrecalculatedContext([bot]);
                const shape = calculateLabelFontSize(calc, bot);
                expect(shape).toBe(0.001);
            });
            it('should return 0.001 for 0.0001', () => {
                const bot = createBot('test', {
                    [tag]: 0.0001,
                });
                const calc = createPrecalculatedContext([bot]);
                const shape = calculateLabelFontSize(calc, bot);
                expect(shape).toBe(0.001);
            });
        });
        it('should default to auto', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = calculateLabelFontSize(calc, bot);
            expect(shape).toBe('auto');
        });
    });
    describe('calculateLabelWordWrapMode()', () => {
        const cases = [['breakCharacters'], ['breakWords'], ['none']];
        const tagCases = ['auxLabelWordWrapMode', 'labelWordWrapMode'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('should return %s', (mode) => {
                const bot = createBot('test', {
                    [tag]: mode,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(calculateLabelWordWrapMode(calc, bot)).toBe(mode);
            });
        });
        it('should default to breakCharacters', () => {
            const bot = createBot();
            const calc = createPrecalculatedContext([bot]);
            const shape = calculateLabelWordWrapMode(calc, bot);
            expect(shape).toBe('breakCharacters');
        });
    });
    describe('getTagPosition()', () => {
        it('should return the contextX, contextY, and contextZ values', () => {
            const bot = createBot('test', {
                tagPositionX: 10,
                tagPositionY: 11,
                tagPositionZ: 12,
            });
            expect(getTagPosition(bot, 'tagPosition')).toEqual({
                x: 10,
                y: 11,
                z: 12,
            });
        });
        it('should support using Vector2 values for position', () => {
            const bot = createBot('test', {
                tagPosition: '➡️1,2',
            });
            expect(getTagPosition(bot, 'tagPosition')).toEqual({
                x: 1,
                y: 2,
                z: 0,
            });
        });
        it('should support using Vector3 values for position', () => {
            const bot = createBot('test', {
                tagPosition: '➡️1,2,3',
            });
            expect(getTagPosition(bot, 'tagPosition')).toEqual({
                x: 1,
                y: 2,
                z: 3,
            });
        });
    });
    describe('getTagRotation()', () => {
        it('should return the contextRotationX, contextRotationY, and contextRotationZ values', () => {
            const bot = createBot('test', {
                tagRotationX: 10,
                tagRotationY: 11,
                tagRotationZ: 12,
            });
            const expectedRotation = new Rotation({
                euler: {
                    x: 10,
                    y: 11,
                    z: 12,
                },
            });
            expect(getTagRotation(bot, 'tagRotation')).toEqual(expectedRotation);
        });
        it('should support separate tags as quaternions', () => {
            const bot = createBot('test', {
                tagRotationX: 10,
                tagRotationY: 11,
                tagRotationZ: 12,
                tagRotationW: 13,
            });
            const expectedRotation = new Rotation({
                quaternion: new Quaternion(10, 11, 12, 13),
            });
            expect(getTagRotation(bot, 'tagRotation')).toEqual(expectedRotation);
        });
        it('should support using Quaternion values for rotation', () => {
            const bot = createBot('test', {
                // 30 degrees around Y
                tagRotation: '🔁0,0.25881904510252074,0,0.9659258262890683',
            });
            expect(getTagRotation(bot, 'tagRotation')).toEqual(new Rotation(new Quaternion(0, 0.25881904510252074, 0, 0.9659258262890683)));
        });
    });
    describe('getBotPosition()', () => {
        it('should return the contextX, contextY, and contextZ values', () => {
            const bot = createBot('test', {
                dimensionX: 10,
                dimensionY: 11,
                dimensionZ: 12,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotPosition(calc, bot, 'dimension')).toEqual({
                x: 10,
                y: 11,
                z: 12,
            });
        });
        it('should support using Vector2 values for position', () => {
            const bot = createBot('test', {
                dimensionPosition: '➡️1,2',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotPosition(calc, bot, 'dimension')).toEqual({
                x: 1,
                y: 2,
                z: 0,
            });
        });
        it('should support using Vector3 values for position', () => {
            const bot = createBot('test', {
                dimensionPosition: '➡️1,2,3',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotPosition(calc, bot, 'dimension')).toEqual({
                x: 1,
                y: 2,
                z: 3,
            });
        });
    });
    describe('getBotRotation()', () => {
        it('should return the contextRotationX, contextRotationY, and contextRotationZ values', () => {
            const bot = createBot('test', {
                dimensionRotationX: 10,
                dimensionRotationY: 11,
                dimensionRotationZ: 12,
            });
            const calc = createPrecalculatedContext([bot]);
            const expectedRotation = new Rotation({
                euler: {
                    x: 10,
                    y: 11,
                    z: 12,
                },
            });
            expect(getBotRotation(calc, bot, 'dimension')).toEqual(expectedRotation);
        });
        it('should support using Quaternion values for rotation', () => {
            const bot = createBot('test', {
                // 30 degrees around Y
                dimensionRotation: '🔁0,0.25881904510252074,0,0.9659258262890683',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotRotation(calc, bot, 'dimension')).toEqual(new Rotation(new Quaternion(0, 0.25881904510252074, 0, 0.9659258262890683)));
        });
    });
    describe('getBotScale()', () => {
        it('should return the scaleX, scaleY, and scaleZ values', () => {
            const bot = createBot('test', {
                scaleX: 10,
                scaleY: 11,
                scaleZ: 12,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotScale(calc, bot)).toEqual({
                x: 10,
                y: 11,
                z: 12,
            });
        });
        it('should cache the result', () => {
            const bot = createBot('test', {
                scaleX: 10,
                scaleY: 11,
                scaleZ: 12,
            });
            const calc = createPrecalculatedContext([bot]);
            const calc2 = createPrecalculatedContext([bot]);
            expect(getBotScale(calc, bot)).toBe(getBotScale(calc, bot));
            expect(getBotScale(calc, bot)).not.toBe(getBotScale(calc2, bot));
        });
        it('should use min scale if 0', () => {
            const bot = createBot('test', {
                scaleX: 0,
                scaleY: 0,
                scaleZ: 0,
                scale: 0,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotScale(calc, bot)).toEqual({
                x: 0.000000000000000001,
                y: 0.000000000000000001,
                z: 0.000000000000000001,
            });
        });
        it('should support negitive numbers', () => {
            const bot = createBot('test', {
                scaleX: -1,
                scaleY: -1,
                scaleZ: -1,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotScale(calc, bot)).toEqual({
                x: -1,
                y: -1,
                z: -1,
            });
        });
    });
    describe('getPortalConfigBotID()', () => {
        it('should return the bot ID that the config bot tag points to', () => {
            const userBot = createBot('userBot', {
                gridPortal: 'abc',
                gridPortalConfigBot: 'test',
            });
            const calc = createPrecalculatedContext([userBot]);
            const id = getPortalConfigBotID(calc, userBot, 'gridPortal');
            expect(id).toEqual('test');
        });
        it('should return null if the tag does not exist', () => {
            const userBot = createBot('userBot', {
                gridPortal: 'abc',
            });
            const calc = createPrecalculatedContext([userBot]);
            const id = getPortalConfigBotID(calc, userBot, 'gridPortal');
            expect(id).toEqual(null);
        });
    });
    describe('getBotTheme()', () => {
        const cases = [
            ['dark', 'dark'],
            ['light', 'light'],
            ['auto', 'auto'],
            [null, 'auto'],
            ['wrong', 'auto'],
        ];
        it.each(cases)('should map %s to %s', (given, expected) => {
            const userBot = createBot('userBot', {
                theme: given,
            });
            const calc = createPrecalculatedContext([userBot]);
            const id = getBotTheme(calc, userBot);
            expect(id).toEqual(expected);
        });
    });
    describe('botDimensionSortOrder()', () => {
        it('should return the dimensionSortOrder tag', () => {
            const bot = createBot('bot', {
                dimensionSortOrder: 123,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(botDimensionSortOrder(calc, bot, 'dimension')).toEqual(123);
        });
    });
    describe('getUserMenuId()', () => {
        it('should return the value from menuPortal', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const calc = createPrecalculatedContext([user]);
            const id = getUserMenuId(calc, user);
            expect(id).toBe('dimension');
        });
    });
    describe('getBotsInMenu()', () => {
        it('should return the list of bots in the users menu', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const bot1 = createBot('bot1', {
                dimension: true,
                dimensionSortOrder: 0,
            });
            const bot2 = createBot('bot2', {
                dimension: true,
                dimensionSortOrder: 1,
            });
            const bot3 = createBot('bot3', {
                dimension: true,
                dimensionSortOrder: 2,
            });
            const calc = createPrecalculatedContext([user, bot2, bot1, bot3]);
            const bots = getBotsInMenu(calc, user);
            expect(bots).toEqual([bot1, bot2, bot3]);
        });
    });
    describe('getChannelBotById()', () => {
        it('should return the first bot that matches', () => {
            const channel = createBot('channel', {
                server: 'test',
                'aux.channels': true,
            });
            const calc = createPrecalculatedContext([channel]);
            const bot = getChannelBotById(calc, 'test');
            expect(bot).toEqual(channel);
        });
        it('should return null if there are no matches', () => {
            const channel = createBot('channel', {
                server: 'test',
                'aux.channels': true,
            });
            const calc = createPrecalculatedContext([channel]);
            const bot = getChannelBotById(calc, 'other');
            expect(bot).toEqual(null);
        });
    });
    describe('addBotToMenu()', () => {
        it('should return the update needed to add the given bot ID to the given users menu', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([user, bot]);
            const update = addBotToMenu(calc, user, 'item');
            expect(update).toEqual({
                tags: {
                    dimension: true,
                    dimensionSortOrder: 0,
                    dimensionId: 'item',
                },
            });
        });
        it('should return the given sortOrder', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([user, bot]);
            const update = addBotToMenu(calc, user, 'item', 5);
            expect(update).toEqual({
                tags: {
                    dimension: true,
                    dimensionSortOrder: 5,
                    dimensionId: 'item',
                },
            });
        });
        it('should return sortOrder needed to place the bot at the end of the list', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const bot = createBot('bot');
            const bot2 = createBot('bot2', {
                dimension: true,
            });
            const calc = createPrecalculatedContext([user, bot, bot2]);
            const update = addBotToMenu(calc, user, 'abc');
            expect(update).toEqual({
                tags: {
                    dimension: true,
                    dimensionSortOrder: 1,
                    dimensionId: 'abc',
                },
            });
        });
    });
    describe('removeBotFromMenu()', () => {
        it('should return the update needed to remove the given bot from the users menu', () => {
            const user = createBot('user', {
                menuPortal: 'dimension',
            });
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([user, bot]);
            const update = removeBotFromMenu(calc, user);
            expect(update).toEqual({
                tags: {
                    dimension: null,
                    dimensionSortOrder: null,
                    dimensionId: null,
                },
            });
        });
    });
    describe('getContextVisualizeMode()', () => {
        const cases = [
            ['surface', 'surface'],
            ['true', true],
            ['false', false],
            [0, false],
            [1, false],
            ['anything', false],
        ];
        it.each(cases)('should map %s to %s', (given, expected) => {
            const bot = createBot('bot', {
                auxDimensionVisualize: given,
            });
            const calc = createPrecalculatedContext([bot]);
            const visible = getDimensionVisualizeMode(calc, bot);
            expect(visible).toBe(expected);
        });
    });
    describe('getContextGrid()', () => {
        it('should find all the tags that represent a grid position', () => {
            const bot = createBot('bot', {
                'auxDimensionConfig.surface.grid.0:1': 1,
                'auxDimensionConfig.surface.grid.1:1': 1,
                'auxDimensionConfig.surface.grid.2:1': 2,
                'auxDimensionConfig.surface.grid.2:2': '3',
            });
            const calc = createPrecalculatedContext([bot]);
            const grid = getBuilderDimensionGrid(calc, bot);
            expect(grid).toEqual({
                '0:1': 1,
                '1:1': 1,
                '2:1': 2,
                '2:2': 3,
            });
        });
        it('should not get confused by grid scale', () => {
            const bot = createBot('bot', {
                'auxDimensionConfig.surface.grid.0:1': 1,
                auxPortalGridScale: 50,
            });
            const calc = createPrecalculatedContext([bot]);
            const grid = getBuilderDimensionGrid(calc, bot);
            expect(grid).toEqual({
                '0:1': 1,
            });
        });
    });
    describe('getContextSize()', () => {
        it('should return the default size if none exists', () => {
            const bot = createBot('bot', {
                auxDimensionVisualize: 'surface',
            });
            const calc = createPrecalculatedContext([bot]);
            const size = getDimensionSize(calc, bot);
            expect(size).toBe(1);
        });
        it('should return the default if the bot is a user bot', () => {
            const bot = createBot('bot', {
                auxPlayerName: 'user',
                auxDimensionVisualize: 'surface',
            });
            const calc = createPrecalculatedContext([bot]);
            const size = getDimensionSize(calc, bot);
            expect(size).toBe(1);
        });
        it('should still return the user bots dimension size', () => {
            const bot = createBot('bot', {
                auxPlayerName: 'user',
                auxDimensionVisualize: 'surface',
                auxDimensionSurfaceSize: 10,
            });
            const calc = createPrecalculatedContext([bot]);
            const size = getDimensionSize(calc, bot);
            expect(size).toBe(10);
        });
        it('should return 0 if the bot is not a surface', () => {
            const bot = createBot('bot', {
                auxDimensionVisualize: true,
                auxDimensionSurfaceSize: 10,
            });
            const calc = createPrecalculatedContext([bot]);
            const size = getDimensionSize(calc, bot);
            expect(size).toBe(0);
        });
    });
    describe('getContextColor()', () => {
        const tagCases = ['auxPortalColor', 'portalColor'];
        describe.each(tagCases)('%s', (tag) => {
            it('should return the auxPortalColor of the bot', () => {
                const bot = createBot('bot', {
                    [tag]: 'red',
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getDimensionColor(calc, bot)).toBe('red');
            });
        });
    });
    describe('getContextGridScale()', () => {
        const tagCases = ['auxPortalGridScale', 'portalGridScale'];
        describe.each(tagCases)('%s', (tag) => {
            it('should return the auxPortalGridScale of the bot', () => {
                const bot = createBot('bot', {
                    [tag]: 10,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getDimensionGridScale(calc, bot)).toBe(10);
            });
        });
    });
    describe('getContextScale()', () => {
        const tagCases = ['auxPortalSurfaceScale', 'portalSurfaceScale'];
        describe.each(tagCases)('%s', (tag) => {
            it('should return the auxPortalSurfaceScale of the bot', () => {
                const bot = createBot('bot', {
                    [tag]: 10,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getDimensionScale(calc, bot)).toBe(10);
            });
        });
        it('should return the default surface scale if the tag is not set', () => {
            const bot = createBot('bot', {});
            const calc = createPrecalculatedContext([bot]);
            expect(getDimensionScale(calc, bot)).toBe(DEFAULT_WORKSPACE_SCALE);
        });
    });
    describe('getContextDefaultHeight()', () => {
        const tagCases = [
            'auxPortalSurfaceDefaultHeight',
            'portalSurfaceDefaultHeight',
        ];
        describe.each(tagCases)('%s', (tag) => {
            it('should return the auxPortalSurfaceDefaultHeight of the bot', () => {
                const bot = createBot('bot', {
                    auxPortalSurfaceDefaultHeight: 10.123,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getDimensionDefaultHeight(calc, bot)).toBe(10.123);
            });
        });
        it('should return undefined if the tag is not set', () => {
            const bot = createBot('bot', {});
            const calc = createPrecalculatedContext([bot]);
            expect(getDimensionDefaultHeight(calc, bot)).toBeUndefined();
        });
    });
    describe('calculateStringListTagValue()', () => {
        it('should return the list contained in the tag with each value converted to a string', () => {
            const bot = createBot('test', {
                tag: ['abc', '', {}, [], false, 0, null, undefined],
            });
            const calc = createPrecalculatedContext([bot]);
            const result = calculateStringListTagValue(calc, bot, 'tag', []);
            expect(result).toEqual([
                'abc',
                '',
                '[object Object]',
                '',
                'false',
                '0',
                null,
                undefined,
            ]);
        });
        it('should return the default value if the list doesnt exist', () => {
            const bot = createBot('test', {});
            const calc = createPrecalculatedContext([bot]);
            const result = calculateStringListTagValue(calc, bot, 'tag', [
                'hello',
            ]);
            expect(result).toEqual(['hello']);
        });
        it('should return the default value if the tag contains an empty string', () => {
            const bot = createBot('test', {
                tag: '',
            });
            const calc = createPrecalculatedContext([bot]);
            const result = calculateStringListTagValue(calc, bot, 'tag', [
                'hello',
            ]);
            expect(result).toEqual(['hello']);
        });
        let cases = [
            [1.1, ['1.1']],
            [false, ['false']],
            ['abc', ['abc']],
            [['abc'], ['abc']],
        ];
        it.each(cases)('should convert %s', (value, expected) => {
            const bot = createBot('test', {
                tag: value,
            });
            const calc = createPrecalculatedContext([bot]);
            const result = calculateStringListTagValue(calc, bot, 'tag', []);
            expect(result).toEqual(expected);
        });
    });
    describe('addToContextDiff()', () => {
        it('should return the tags needed to add a bot to a dimension', () => {
            const bot = createBot('bot', {});
            const calc = createPrecalculatedContext([bot]);
            const tags = addToDimensionDiff(calc, 'test');
            expect(tags).toEqual({
                test: true,
                testX: 0,
                testY: 0,
                testSortOrder: 0,
            });
        });
        it('should calculate the sortOrder', () => {
            const bot = createBot('bot', {});
            const bot2 = createBot('bot2', {
                test: true,
                testSortOrder: 0,
            });
            const calc = createPrecalculatedContext([bot, bot2]);
            const tags = addToDimensionDiff(calc, 'test');
            expect(tags).toEqual({
                test: true,
                testX: 0,
                testY: 0,
                testSortOrder: 1,
            });
        });
        it('should calculate the sortOrder based on the given position', () => {
            const bot = createBot('bot', {});
            const bot2 = createBot('bot2', {
                test: true,
                testSortOrder: 0,
                testX: 0,
                testY: 0,
            });
            const calc = createPrecalculatedContext([bot, bot2]);
            const tags = addToDimensionDiff(calc, 'test', 1, 2);
            expect(tags).toEqual({
                test: true,
                testX: 1,
                testY: 2,
                testSortOrder: 0,
            });
        });
    });
    describe('removeFromContextDiff()', () => {
        it('should return the tags needed to remove a bot from a dimension', () => {
            const calc = createPrecalculatedContext([]);
            const tags = removeFromDimensionDiff(calc, 'test');
            expect(tags).toEqual({
                test: null,
                testX: null,
                testY: null,
                testSortOrder: null,
            });
        });
    });
    describe('isContextMovable()', () => {
        it('should return true if movable', () => {
            const bot = createBot('test', {
                abc: true,
                auxDimensionConfig: 'abc',
                auxDimensionSurfaceMovable: true,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(isDimensionMovable(calc, bot)).toBe(true);
        });
        it('should return false if not movable', () => {
            const bot = createBot('test', {
                abc: true,
                auxDimensionConfig: 'abc',
                auxDimensionSurfaceMovable: false,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(isDimensionMovable(calc, bot)).toBe(false);
        });
        it('should be movable by default', () => {
            const bot = createBot('test', {});
            const calc = createPrecalculatedContext([bot]);
            expect(isDimensionMovable(calc, bot)).toBe(true);
        });
    });
    describe('isContext()', () => {
        it('should return true when the given bot has auxDimensionConfig set to something', () => {
            const bot = createBot('test', {
                auxDimensionConfig: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(isDimension(calc, bot)).toBe(true);
        });
        it('should return false when the given bot does not have auxDimensionConfig set to something', () => {
            const bot = createBot('test', {
                auxDimensionConfig: '',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(isDimension(calc, bot)).toBe(false);
        });
    });
    describe('getBotConfigContexts()', () => {
        it('should return the list of values in auxDimensionConfig', () => {
            const bot = createBot('test', {
                abc: true,
                auxDimensionConfig: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            const tags = getBotConfigDimensions(calc, bot);
            expect(tags).toEqual(['abc']);
        });
        it('should return the list of values when given a number', () => {
            const bot = createBot('test', {
                abc: true,
                auxDimensionConfig: 123,
            });
            const calc = createPrecalculatedContext([bot]);
            const tags = getBotConfigDimensions(calc, bot);
            expect(tags).toEqual(['123']);
        });
        it('should return the list of values when given a boolean', () => {
            const bot = createBot('test', {
                abc: true,
                auxDimensionConfig: false,
            });
            const calc = createPrecalculatedContext([bot]);
            const tags = getBotConfigDimensions(calc, bot);
            expect(tags).toEqual(['false']);
        });
    });
    describe('isContextLocked()', () => {
        it('should default to false when the bot is a dimension', () => {
            const bot = createBot('test', {
                auxDimensionConfig: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            const locked = isDimensionLocked(calc, bot);
            expect(locked).toEqual(false);
        });
    });
    describe('getBotLabelAnchor()', () => {
        it('should default to top', () => {
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([bot]);
            const anchor = getBotLabelAnchor(calc, bot);
            expect(anchor).toBe('top');
        });
        const cases = [
            ['top', 'top'],
            ['front', 'front'],
            ['back', 'back'],
            ['left', 'left'],
            ['right', 'right'],
            ['floating', 'floating'],
            ['floatingBillboard', 'floatingBillboard'],
            ['abc', 'top'],
        ];
        describe('auxLabelPosition', () => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    auxLabelPosition: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotLabelAnchor(calc, bot);
                expect(a).toBe(expected);
            });
        });
        describe('labelPosition', () => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    labelPosition: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotLabelAnchor(calc, bot);
                expect(a).toBe(expected);
            });
        });
    });
    describe('getBotLabelAlignment()', () => {
        it('should default to center', () => {
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([bot]);
            const anchor = getBotLabelAlignment(calc, bot);
            expect(anchor).toBe('center');
        });
        const cases = [
            ['center', 'center'],
            ['left', 'left'],
            ['right', 'right'],
            ['abc', 'center'],
        ];
        describe('auxLabelAlignment', () => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    auxLabelAlignment: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotLabelAlignment(calc, bot);
                expect(a).toBe(expected);
            });
        });
        describe('labelAlignment', () => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    labelAlignment: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotLabelAlignment(calc, bot);
                expect(a).toBe(expected);
            });
        });
    });
    describe('getBotLabelPadding()', () => {
        const tagCases = ['auxLabelPadding', 'labelPadding'];
        describe.each(tagCases)('%s', (tag) => {
            numericalTagValueTests(0, (given, expected) => {
                const bot = createBot('test', {
                    [tag]: given,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotLabelPadding(calc, bot)).toEqual({
                    horizontal: expected,
                    vertical: expected,
                });
            });
            const outOfBoundsTests = [
                ['Infinity', Infinity],
                ['Negative Infinity', -Infinity],
                ['NaN', NaN],
            ];
            it.each(outOfBoundsTests)('should map %s to 0', (desc, value) => {
                const bot = createBot('test', {
                    [tag]: value,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotLabelPadding(calc, bot)).toEqual({
                    horizontal: 0,
                    vertical: 0,
                });
            });
            it('should return 1 by default', () => {
                const bot = createBot('test', {
                    [tag]: null,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotLabelPadding(calc, bot)).toEqual({
                    horizontal: 0,
                    vertical: 0,
                });
            });
            it('should add the builtin and vertical/horizontal paddings together', () => {
                const bot = createBot('test', {
                    [tag]: 2,
                    [tag + 'Y']: 1,
                    [tag + 'X']: 3,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotLabelPadding(calc, bot)).toEqual({
                    horizontal: 5,
                    vertical: 3,
                });
            });
            const separatePaddingValuesTests = [
                [13, 21, { vertical: 13, horizontal: 21 }],
                [3.14159, 0.2, { vertical: 3.14159, horizontal: 0.2 }],
                [3.14159, null, { vertical: 3.14159, horizontal: 0 }],
                [0.123, 1000, { vertical: 0.123, horizontal: 1000 }],
                [null, 1000, { vertical: 0, horizontal: 1000 }],
                [1000, 0, { vertical: 1000, horizontal: 0 }],
                [1000, 123, { vertical: 1000, horizontal: 123 }],
            ];
            it.each(separatePaddingValuesTests)('should support %s', (vertical, horizontal, expected) => {
                const bot = createBot('test', {
                    [tag + 'Y']: vertical,
                    [tag + 'X']: horizontal,
                });
                const calc = createPrecalculatedContext([bot]);
                expect(getBotLabelPadding(calc, bot)).toEqual(expected);
            });
        });
    });
    describe('getBotScaleMode()', () => {
        it('should default to fit', () => {
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([bot]);
            const anchor = getBotScaleMode(calc, bot);
            expect(anchor).toBe('fit');
        });
        const cases = [
            ['fit', 'fit'],
            ['absolute', 'absolute'],
            ['abc', 'fit'],
        ];
        const tagCases = ['auxScaleMode', 'scaleMode'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    [tag]: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotScaleMode(calc, bot);
                expect(a).toBe(expected);
            });
        });
    });
    describe('getBotMeshPositioningMode()', () => {
        it('should default to center', () => {
            const bot = createBot('bot');
            const calc = createPrecalculatedContext([bot]);
            const anchor = getBotMeshPositioningMode(calc, bot);
            expect(anchor).toBe('center');
        });
        const cases = [
            ['center', 'center'],
            ['absolute', 'absolute'],
            ['abc', 'center'],
        ];
        const tagCases = ['auxMeshPositioningMode', 'meshPositioningMode'];
        describe.each(tagCases)('%s', (tag) => {
            it.each(cases)('given %s it should return %s', (anchor, expected) => {
                const bot = createBot('bot', {
                    [tag]: anchor,
                });
                const calc = createPrecalculatedContext([bot]);
                const a = getBotMeshPositioningMode(calc, bot);
                expect(a).toBe(expected);
            });
        });
    });
    describe('getBotVersion()', () => {
        it('should return the auxVersion', () => {
            const bot = createBot('test', {
                auxVersion: 1,
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotVersion(calc, bot)).toBe(1);
        });
        it('should return undefined if not a number', () => {
            const bot = createBot('test', {
                auxVersion: 'abc',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getBotVersion(calc, bot)).toBeUndefined();
        });
    });
    describe('isBotInDimension()', () => {
        it('should handle boolean objects', () => {
            const thisBot = createBot('thisBot', {
                dimension: new Boolean(true),
            });
            const calc = createPrecalculatedContext([thisBot]);
            const result = isBotInDimension(calc, thisBot, 'dimension');
            expect(result).toBe(true);
        });
        it('should handle a string object as the dimension', () => {
            const thisBot = createBot('thisBot', {
                dimension: true,
            });
            const calc = createPrecalculatedContext([thisBot]);
            const result = isBotInDimension(calc, thisBot, new String('dimension'));
            expect(result).toBe(true);
        });
        booleanTagValueTests(false, (given, expected) => {
            const thisBot = createBot('thisBot', {
                dimension: given,
            });
            const calc = createPrecalculatedContext([thisBot]);
            const result = isBotInDimension(calc, thisBot, 'dimension');
            expect(result).toBe(expected);
        });
        const cases = [
            ['true', true],
            ['false', false],
            ['abc', false],
            [0, false],
            [1, false],
            [{}, false],
            [null, false],
            [undefined, false],
            [true, true],
            [false, false],
        ];
        it.each(cases)('should handle %s', (given, expected) => {
            const thisBot = createBot('thisBot', {
                dimension: given,
            });
            const calc = createPrecalculatedContext([thisBot]);
            const result = isBotInDimension(calc, thisBot, 'dimension');
            expect(result).toBe(expected);
        });
    });
    describe('isBotPointable()', () => {
        const tagCases = ['auxPointable', 'pointable'];
        describe.each(tagCases)('%s', (tag) => {
            booleanTagValueTests(true, (given, expected) => {
                const thisBot = createBot('thisBot', {
                    [tag]: given,
                });
                const calc = createPrecalculatedContext([thisBot]);
                const result = isBotPointable(calc, thisBot);
                expect(result).toBe(expected);
            });
        });
    });
    describe('isBotFocusable()', () => {
        const tagCases = ['auxFocusable', 'focusable'];
        describe.each(tagCases)('%s', (tag) => {
            booleanTagValueTests(true, (given, expected) => {
                const thisBot = createBot('thisBot', {
                    [tag]: given,
                });
                const calc = createPrecalculatedContext([thisBot]);
                const result = isBotFocusable(calc, thisBot);
                expect(result).toBe(expected);
            });
        });
    });
    describe('getUserBotColor()', () => {
        const defaultCases = [
            [DEFAULT_BUILDER_USER_COLOR, 'builder'],
            [DEFAULT_PLAYER_USER_COLOR, 'player'],
        ];
        it.each(defaultCases)('should default to %s when in %s', (expected, domain) => {
            const bot = createBot('test', {});
            const calc = createPrecalculatedContext([bot]);
            expect(getUserBotColor(calc, bot, domain)).toBe(expected);
        });
        const userCases = [['player'], ['builder']];
        it.each(userCases)('should use auxColor from the user bot', (domain) => {
            const bot = createBot('test', {
                auxColor: 'red',
            });
            const calc = createPrecalculatedContext([bot]);
            expect(getUserBotColor(calc, bot, domain)).toBe('red');
        });
    });
    describe('calculateDestroyBotEvents()', () => {
        it('should return a list of events needed to destroy the given bot', () => {
            const bot1 = createBot('bot1');
            const bot2 = createBot('bot2', {
                creator: 'bot1',
            });
            const bot3 = createBot('bot3', {
                creator: 'bot2',
            });
            const bot4 = createBot('bot4', {
                creator: 'bot1',
            });
            const bot5 = createBot('bot5');
            const calc = createPrecalculatedContext([bot1, bot2, bot3, bot4, bot5], undefined);
            const events = calculateDestroyBotEvents(calc, bot1);
            expect(events).toEqual([
                botRemoved('bot1'),
                botRemoved('bot2'),
                botRemoved('bot3'),
                botRemoved('bot4'),
            ]);
        });
        it('should not return a destroy event for bots that are not destroyable', () => {
            const bot1 = createBot('bot1');
            const bot2 = createBot('bot2', {
                creator: 'bot1',
                destroyable: false,
            });
            const bot3 = createBot('bot3', {
                creator: 'bot2',
            });
            const bot4 = createBot('bot4', {
                creator: 'bot1',
            });
            const bot5 = createBot('bot5');
            const calc = createPrecalculatedContext([bot1, bot2, bot3, bot4, bot5], undefined);
            const events = calculateDestroyBotEvents(calc, bot1);
            expect(events).toEqual([
                botRemoved('bot1'),
                // bot2 and bot3 are not destroyed because they are not destroyable
                botRemoved('bot4'),
            ]);
        });
        it('should handle circular creators', () => {
            const bot1 = createBot('bot1', {
                creator: 'bot5',
            });
            const bot2 = createBot('bot2', {
                creator: 'bot1',
            });
            const bot3 = createBot('bot3', {
                creator: 'bot2',
            });
            const bot4 = createBot('bot4', {
                creator: 'bot3',
            });
            const bot5 = createBot('bot5');
            const calc = createPrecalculatedContext([bot1, bot2, bot3, bot4, bot5], undefined);
            const events = calculateDestroyBotEvents(calc, bot1);
            expect(events).toEqual([
                botRemoved('bot1'),
                botRemoved('bot2'),
                botRemoved('bot3'),
                botRemoved('bot4'),
            ]);
        });
    });
    describe('resolveRejectedActions()', () => {
        it('should remove an action if it has been rejected after it was issued', () => {
            let toastAction = toast('abc');
            let actions = [toastAction, reject(toastAction)];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([]);
        });
        it('should keep an action if it has been rejected before it was issued', () => {
            let toastAction = toast('abc');
            let actions = [reject(toastAction), toastAction];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([toast('abc')]);
        });
        it('should be able to remove a rejection', () => {
            let toastAction = toast('abc');
            let rejection = reject(toastAction);
            let actions = [toastAction, rejection, reject(rejection)];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([toast('abc')]);
        });
        it('should preserve the order of the original actions', () => {
            let actions = [toast('abc'), toast('def')];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([toast('abc'), toast('def')]);
        });
        it('should handle rejecting an action twice', () => {
            let toastAction = toast('abc');
            let actions = [
                toastAction,
                reject(toastAction),
                reject(toastAction),
            ];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([]);
        });
        it('should remove duplicate actions', () => {
            let toastAction = toast('abc');
            let actions = [toastAction, toastAction];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([toastAction]);
        });
        it('should allow an action if it is re-added after it is rejected', () => {
            let toastAction = toast('abc');
            let actions = [toastAction, reject(toastAction), toastAction];
            const final = resolveRejectedActions(actions);
            expect(final).toEqual([toastAction]);
        });
    });
    describe('BotLookupTable', () => {
        describe('buildLookupTable()', () => {
            const dataTypes = [
                ['support strings', 'jkl', 'pqr'],
                ['support integers', 123, 456],
                ['support floats', 123.65, 456.789],
                ['support booleans', false, true],
                ['support nulls', null, null],
                ['support mixed types', 'hello', 987],
                ['support zero', 0, 0],
            ];
            it.each(dataTypes)('should %s (%s, %s)', (desc, first, second) => {
                const test = createBot('test', {
                    abc: 'def',
                    ghi: first,
                    mno: second,
                });
                const test2 = createBot('test2', {
                    abc: 123,
                    ghi: first,
                    mno: second,
                });
                const test3 = createBot('test3', {
                    abc: 123,
                    ghi: 'jkl',
                    mno: true,
                });
                const test4 = createBot('test4', {
                    abc: 'def',
                    ghi: 456,
                    mno: true,
                });
                const calc = createPrecalculatedContext([
                    test,
                    test2,
                    test3,
                    test4,
                ]);
                const table = buildLookupTable(calc, ['ghi', 'mno']);
                const results = table.query([first, second]);
                expect(results).toEqual([test, test2]);
            });
            it('should use the given default values when a tag is missing', () => {
                const test = createBot('test', {
                    abc: 'def',
                });
                const calc = createPrecalculatedContext([test]);
                const table = buildLookupTable(calc, ['missing'], [99]);
                const results = table.query([99]);
                expect(results).toEqual([test]);
            });
        });
    });
    describe('BotLookupTableHelper', () => {
        describe('query()', () => {
            it('should build a lookup table from the given list of tags', () => {
                const test = createBot('test', {
                    abc: 'def',
                    ghi: 'jkl',
                    mno: 'pqr',
                });
                const test2 = createBot('test2', {
                    abc: 123,
                    ghi: 'jkl',
                    mno: 'pqr',
                });
                const test3 = createBot('test3', {
                    abc: 123,
                    ghi: 'jkl',
                    mno: true,
                });
                const test4 = createBot('test4', {
                    abc: 'def',
                    ghi: 456,
                    mno: true,
                });
                const calc = createPrecalculatedContext([
                    test,
                    test2,
                    test3,
                    test4,
                ]);
                const helper = new BotLookupTableHelper();
                const results = helper.query(calc, ['ghi', 'mno'], ['jkl', 'pqr']);
                expect(results).toEqual([test, test2]);
            });
            it('should return the same values even if the tag order changes', () => {
                const test = createBot('test', {
                    abc: 'def',
                    ghi: 'jkl',
                    mno: 'pqr',
                });
                const test2 = createBot('test2', {
                    abc: 123,
                    ghi: 'jkl',
                    mno: 'pqr',
                });
                const test3 = createBot('test3', {
                    abc: 123,
                    ghi: 'jkl',
                    mno: true,
                });
                const test4 = createBot('test4', {
                    abc: 'def',
                    ghi: 456,
                    mno: true,
                });
                const calc = createPrecalculatedContext([
                    test,
                    test2,
                    test3,
                    test4,
                ]);
                const helper = new BotLookupTableHelper();
                const results = helper.query(calc, ['mno', 'ghi'], ['pqr', 'jkl']);
                expect(results).toEqual([test, test2]);
            });
        });
    });
}
//# sourceMappingURL=BotCalculationContextTests.js.map