Fix: aiscriptディレクトリ内の型エラー解消と単体テスト (#15191)

* AiScript APIの型エラーに対処

* AiScript UI APIのテスト作成

* onInputなどがPromiseを返すように

* AiScript共通APIのテスト作成

* CHANGELOG記載

* 定数のテストをconcurrentに

* vi.mockを使用

* misskeyApiをmisskeyApiUntypedのエイリアスとする

* 期待されるエラーメッセージを修正

* Mk:removeのテスト

* misskeyApiの型を変更
This commit is contained in:
Take-John
2025-01-07 21:28:48 +09:00
committed by GitHub
parent f7da2bad6f
commit bbe80af1dd
8 changed files with 1396 additions and 64 deletions

View File

@@ -0,0 +1,401 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { miLocalStorage } from '@/local-storage.js';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
import {
afterAll,
afterEach,
beforeAll,
beforeEach,
describe,
expect,
test,
vi
} from 'vitest';
async function exe(script: string): Promise<values.Value[]> {
const outputs: values.Value[] = [];
const interpreter = new Interpreter(
createAiScriptEnv({ storageKey: 'widget' }),
{
in: aiScriptReadline,
out: (value) => {
outputs.push(value);
}
}
);
const ast = Parser.parse(script);
await interpreter.exec(ast);
return outputs;
}
let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >(
() => null
);
vi.mock('@/account.js', () => {
return {
get $i() {
return $iMock;
},
};
});
const osMock = vi.hoisted(() => {
return {
inputText: vi.fn(),
alert: vi.fn(),
confirm: vi.fn(),
};
});
vi.mock('@/os.js', () => {
return osMock;
});
const misskeyApiMock = vi.hoisted(() => vi.fn());
vi.mock('@/scripts/misskey-api.js', () => {
return { misskeyApi: misskeyApiMock };
});
describe('AiScript common API', () => {
afterAll(() => {
vi.unstubAllGlobals();
});
describe('readline', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.inputText.mockImplementationOnce(async ({ title }) => {
expect(title).toBe('question');
return {
canceled: false,
result: 'Hello',
};
});
const [res] = await exe(`
<: readline('question')
`);
expect(res).toStrictEqual(values.STR('Hello'));
expect(osMock.inputText).toHaveBeenCalledOnce();
});
test.sequential('cancelled', async () => {
osMock.inputText.mockImplementationOnce(async ({ title }) => {
expect(title).toBe('question');
return {
canceled: true,
result: undefined,
};
});
const [res] = await exe(`
<: readline('question')
`);
expect(res).toStrictEqual(values.STR(''));
expect(osMock.inputText).toHaveBeenCalledOnce();
});
});
describe('user constants', () => {
describe.sequential('logged in', () => {
beforeAll(() => {
$iMock = {
id: 'xxxxxxxx',
name: '藍',
username: 'ai',
};
});
test.concurrent('USER_ID', async () => {
const [res] = await exe(`
<: USER_ID
`);
expect(res).toStrictEqual(values.STR('xxxxxxxx'));
});
test.concurrent('USER_NAME', async () => {
const [res] = await exe(`
<: USER_NAME
`);
expect(res).toStrictEqual(values.STR('藍'));
});
test.concurrent('USER_USERNAME', async () => {
const [res] = await exe(`
<: USER_USERNAME
`);
expect(res).toStrictEqual(values.STR('ai'));
});
});
describe.sequential('not logged in', () => {
beforeAll(() => {
$iMock = null;
});
test.concurrent('USER_ID', async () => {
const [res] = await exe(`
<: USER_ID
`);
expect(res).toStrictEqual(values.NULL);
});
test.concurrent('USER_NAME', async () => {
const [res] = await exe(`
<: USER_NAME
`);
expect(res).toStrictEqual(values.NULL);
});
test.concurrent('USER_USERNAME', async () => {
const [res] = await exe(`
<: USER_USERNAME
`);
expect(res).toStrictEqual(values.NULL);
});
});
});
describe('dialog', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('success');
expect(title).toBe('Hello');
expect(text).toBe('world');
});
const [res] = await exe(`
<: Mk:dialog('Hello', 'world', 'success')
`);
expect(res).toStrictEqual(values.NULL);
expect(osMock.alert).toHaveBeenCalledOnce();
});
test.sequential('omit type', async () => {
osMock.alert.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('info');
expect(title).toBe('Hello');
expect(text).toBe('world');
});
const [res] = await exe(`
<: Mk:dialog('Hello', 'world')
`);
expect(res).toStrictEqual(values.NULL);
expect(osMock.alert).toHaveBeenCalledOnce();
});
test.sequential('invalid type', async () => {
await expect(() => exe(`
<: Mk:dialog('Hello', 'world', 'invalid')
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
expect(osMock.alert).not.toHaveBeenCalled();
});
});
describe('confirm', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('ok', async () => {
osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('success');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: false };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world', 'success')
`);
expect(res).toStrictEqual(values.TRUE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('omit type', async () => {
osMock.confirm
.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('question');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: false };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world')
`);
expect(res).toStrictEqual(values.TRUE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('canceled', async () => {
osMock.confirm.mockImplementationOnce(async ({ type, title, text }) => {
expect(type).toBe('question');
expect(title).toBe('Hello');
expect(text).toBe('world');
return { canceled: true };
});
const [res] = await exe(`
<: Mk:confirm('Hello', 'world')
`);
expect(res).toStrictEqual(values.FALSE);
expect(osMock.confirm).toHaveBeenCalledOnce();
});
test.sequential('invalid type', async () => {
const confirm = osMock.confirm;
await expect(() => exe(`
<: Mk:confirm('Hello', 'world', 'invalid')
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
expect(confirm).not.toHaveBeenCalled();
});
});
describe('api', () => {
afterEach(() => {
vi.restoreAllMocks();
});
test.sequential('successful', async () => {
misskeyApiMock.mockImplementationOnce(
async (endpoint, data, token) => {
expect(endpoint).toBe('ping');
expect(data).toStrictEqual({});
expect(token).toBeNull();
return { pong: 1735657200000 };
}
);
const [res] = await exe(`
<: Mk:api('ping', {})
`);
expect(res).toStrictEqual(values.OBJ(new Map([
['pong', values.NUM(1735657200000)],
])));
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('with token', async () => {
misskeyApiMock.mockImplementationOnce(
async (endpoint, data, token) => {
expect(endpoint).toBe('ping');
expect(data).toStrictEqual({});
expect(token).toStrictEqual('xxxxxxxx');
return { pong: 1735657200000 };
}
);
const [res] = await exe(`
<: Mk:api('ping', {}, 'xxxxxxxx')
`);
expect(res).toStrictEqual(values.OBJ(new Map([
['pong', values.NUM(1735657200000 )],
])));
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('request failed', async () => {
misskeyApiMock.mockRejectedValueOnce('Not Found');
const [res] = await exe(`
<: Mk:api('this/endpoint/should/not/be/found', {})
`);
expect(res).toStrictEqual(
values.ERROR('request_failed', values.STR('Not Found'))
);
expect(misskeyApiMock).toHaveBeenCalledOnce();
});
test.sequential('invalid endpoint', async () => {
await expect(() => exe(`
Mk:api('https://example.com/api/ping', {})
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('invalid endpoint'),
);
expect(misskeyApiMock).not.toHaveBeenCalled();
});
test.sequential('missing param', async () => {
await expect(() => exe(`
Mk:api('ping')
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('expected param'),
);
expect(misskeyApiMock).not.toHaveBeenCalled();
});
});
describe('save and load', () => {
beforeEach(() => {
miLocalStorage.removeItem('aiscript:widget:key');
});
afterEach(() => {
miLocalStorage.removeItem('aiscript:widget:key');
});
test.sequential('successful', async () => {
const [res] = await exe(`
Mk:save('key', 'value')
<: Mk:load('key')
`);
expect(miLocalStorage.getItem('aiscript:widget:key')).toBe('"value"');
expect(res).toStrictEqual(values.STR('value'));
});
test.sequential('missing value to save', async () => {
await expect(() => exe(`
Mk:save('key')
`)).rejects.toStrictEqual(
new errors.AiScriptRuntimeError('Expect anything, but got nothing.'),
);
});
test.sequential('not value found to load', async () => {
const [res] = await exe(`
<: Mk:load('key')
`);
expect(res).toStrictEqual(values.NULL);
});
test.sequential('remove existing', async () => {
const res = await exe(`
Mk:save('key', 'value')
<: Mk:load('key')
<: Mk:remove('key')
<: Mk:load('key')
`);
expect(res).toStrictEqual([values.STR('value'), values.NULL, values.NULL]);
});
test.sequential('remove nothing', async () => {
const res = await exe(`
<: Mk:load('key')
<: Mk:remove('key')
<: Mk:load('key')
`);
expect(res).toStrictEqual([values.NULL, values.NULL, values.NULL]);
});
});
test.concurrent('url', async () => {
vi.stubGlobal('location', { href: 'https://example.com/' });
const [res] = await exe(`
<: Mk:url()
`);
expect(res).toStrictEqual(values.STR('https://example.com/'));
});
test.concurrent('nyaize', async () => {
const [res] = await exe(`
<: Mk:nyaize('な')
`);
expect(res).toStrictEqual(values.STR('にゃ'));
});
});

View File

@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { assertStringAndIsIn } from "@/scripts/aiscript/common.js";
import { values } from "@syuilo/aiscript";
import { describe, expect, test } from "vitest";
describe('AiScript common script', () => {
test('assertStringAndIsIn', () => {
expect(
() => assertStringAndIsIn(values.STR('a'), ['a', 'b'])
).not.toThrow();
expect(
() => assertStringAndIsIn(values.STR('c'), ['a', 'b'])
).toThrow('"c" is not in "a", "b"');
expect(() => assertStringAndIsIn(
values.STR('invalid'),
['left', 'center', 'right']
)).toThrow('"invalid" is not in "left", "center", "right"');
});
});

View File

@@ -0,0 +1,825 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
import { describe, expect, test } from 'vitest';
import { type Ref, ref } from 'vue';
import type {
AsUiButton,
AsUiButtons,
AsUiComponent,
AsUiMfm,
AsUiNumberInput,
AsUiRoot,
AsUiSelect,
AsUiSwitch,
AsUiText,
AsUiTextarea,
AsUiTextInput,
} from '@/scripts/aiscript/ui.js';
type ExeResult = {
root: AsUiRoot;
get: (id: string) => AsUiComponent;
outputs: values.Value[];
}
async function exe(script: string): Promise<ExeResult> {
const rootRef = ref<AsUiRoot>();
const componentRefs = ref<Ref<AsUiComponent>[]>([]);
const outputs: values.Value[] = [];
const interpreter = new Interpreter(
registerAsUiLib(componentRefs.value, (root) => {
rootRef.value = root.value;
}),
{
out: (value) => {
outputs.push(value);
}
}
);
const ast = Parser.parse(script);
await interpreter.exec(ast);
const root = rootRef.value;
if (root === undefined) {
expect.unreachable('root must not be undefined');
}
const components = componentRefs.value.map(
(componentRef) => componentRef.value,
);
expect(root).toBe(components[0]);
expect(root.type).toBe('root');
const get = (id: string) => {
const component = componentRefs.value.find(
(componentRef) => componentRef.value.id === id,
);
if (component === undefined) {
expect.unreachable(`component "${id}" is not defined`);
}
return component.value;
};
return { root, get, outputs };
}
describe('AiScript UI API', () => {
test.concurrent('root', async () => {
const { root } = await exe('');
expect(root.children).toStrictEqual([]);
});
describe('get', () => {
test.concurrent('some', async () => {
const { outputs } = await exe(`
Ui:C:text({}, 'id')
<: Ui:get('id')
`);
const output = outputs[0] as values.VObj;
expect(output.type).toBe('obj');
expect(output.value.size).toBe(2);
expect(output.value.get('id')).toStrictEqual(values.STR('id'));
expect(output.value.get('update')!.type).toBe('fn');
});
test.concurrent('none', async () => {
const { outputs } = await exe(`
<: Ui:get('id')
`);
expect(outputs).toStrictEqual([values.NULL]);
});
});
describe('update', () => {
test.concurrent('normal', async () => {
const { get } = await exe(`
let text = Ui:C:text({ text: 'a' }, 'id')
text.update({ text: 'b' })
`);
const text = get('id') as AsUiText;
expect(text.text).toBe('b');
});
test.concurrent('skip unknown key', async () => {
const { get } = await exe(`
let text = Ui:C:text({ text: 'a' }, 'id')
text.update({
text: 'b'
unknown: null
})
`);
const text = get('id') as AsUiText;
expect(text.text).toBe('b');
expect('unknown' in text).toBeFalsy();
});
});
describe('container', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let text = Ui:C:text({
text: 'text'
}, 'id1')
let container = Ui:C:container({
children: [text]
align: 'left'
bgColor: '#fff'
fgColor: '#000'
font: 'sans-serif'
borderWidth: 1
borderColor: '#f00'
borderStyle: 'hidden'
borderRadius: 2
padding: 3
rounded: true
hidden: false
}, 'id2')
Ui:render([container])
`);
expect(root.children).toStrictEqual(['id2']);
expect(get('id2')).toStrictEqual({
type: 'container',
id: 'id2',
children: ['id1'],
align: 'left',
bgColor: '#fff',
fgColor: '#000',
font: 'sans-serif',
borderColor: '#f00',
borderWidth: 1,
borderStyle: 'hidden',
borderRadius: 2,
padding: 3,
rounded: true,
hidden: false,
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:container({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'container',
id: 'id',
children: [],
align: undefined,
fgColor: undefined,
bgColor: undefined,
font: undefined,
borderWidth: undefined,
borderColor: undefined,
borderStyle: undefined,
borderRadius: undefined,
padding: undefined,
rounded: undefined,
hidden: undefined,
});
});
test.concurrent('invalid children', async () => {
await expect(() => exe(`
Ui:C:container({
children: 0
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid align', async () => {
await expect(() => exe(`
Ui:C:container({
align: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:container({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
test.concurrent('invalid borderStyle', async () => {
await expect(() => exe(`
Ui:C:container({
borderStyle: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('text', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let text = Ui:C:text({
text: 'a'
size: 1
bold: true
color: '#000'
font: 'sans-serif'
}, 'id')
Ui:render([text])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'text',
id: 'id',
text: 'a',
size: 1,
bold: true,
color: '#000',
font: 'sans-serif',
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:text({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'text',
id: 'id',
text: undefined,
size: undefined,
bold: undefined,
color: undefined,
font: undefined,
});
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:text({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('mfm', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let mfm = Ui:C:mfm({
text: 'text'
size: 1
bold: true
color: '#000'
font: 'sans-serif'
onClickEv: print
}, 'id')
Ui:render([mfm])
`);
expect(root.children).toStrictEqual(['id']);
const { onClickEv, ...mfm } = get('id') as AsUiMfm;
expect(mfm).toStrictEqual({
type: 'mfm',
id: 'id',
text: 'text',
size: 1,
bold: true,
color: '#000',
font: 'sans-serif',
});
await onClickEv!('a');
expect(outputs).toStrictEqual([values.STR('a')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:mfm({}, 'id')
`);
const { onClickEv, ...mfm } = get('id') as AsUiMfm;
expect(onClickEv).toBeTypeOf('function');
expect(mfm).toStrictEqual({
type: 'mfm',
id: 'id',
text: undefined,
size: undefined,
bold: undefined,
color: undefined,
font: undefined,
});
});
test.concurrent('invalid font', async () => {
await expect(() => exe(`
Ui:C:mfm({
font: 'invalid'
})
`)).rejects.toBeInstanceOf(errors.AiScriptRuntimeError);
});
});
describe('textInput', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let text_input = Ui:C:textInput({
onInput: print
default: 'a'
label: 'b'
caption: 'c'
}, 'id')
Ui:render([text_input])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...textInput } = get('id') as AsUiTextInput;
expect(textInput).toStrictEqual({
type: 'textInput',
id: 'id',
default: 'a',
label: 'b',
caption: 'c',
});
await onInput!('d');
expect(outputs).toStrictEqual([values.STR('d')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:textInput({}, 'id')
`);
const { onInput, ...textInput } = get('id') as AsUiTextInput;
expect(onInput).toBeTypeOf('function');
expect(textInput).toStrictEqual({
type: 'textInput',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('textarea', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let textarea = Ui:C:textarea({
onInput: print
default: 'a'
label: 'b'
caption: 'c'
}, 'id')
Ui:render([textarea])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...textarea } = get('id') as AsUiTextarea;
expect(textarea).toStrictEqual({
type: 'textarea',
id: 'id',
default: 'a',
label: 'b',
caption: 'c',
});
await onInput!('d');
expect(outputs).toStrictEqual([values.STR('d')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:textarea({}, 'id')
`);
const { onInput, ...textarea } = get('id') as AsUiTextarea;
expect(onInput).toBeTypeOf('function');
expect(textarea).toStrictEqual({
type: 'textarea',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('numberInput', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let number_input = Ui:C:numberInput({
onInput: print
default: 1
label: 'a'
caption: 'b'
}, 'id')
Ui:render([number_input])
`);
expect(root.children).toStrictEqual(['id']);
const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
expect(numberInput).toStrictEqual({
type: 'numberInput',
id: 'id',
default: 1,
label: 'a',
caption: 'b',
});
await onInput!(2);
expect(outputs).toStrictEqual([values.NUM(2)]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:numberInput({}, 'id')
`);
const { onInput, ...numberInput } = get('id') as AsUiNumberInput;
expect(onInput).toBeTypeOf('function');
expect(numberInput).toStrictEqual({
type: 'numberInput',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('button', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let button = Ui:C:button({
text: 'a'
onClick: @() { <: 'clicked' }
primary: true
rounded: false
disabled: false
}, 'id')
Ui:render([button])
`);
expect(root.children).toStrictEqual(['id']);
const { onClick, ...button } = get('id') as AsUiButton;
expect(button).toStrictEqual({
type: 'button',
id: 'id',
text: 'a',
primary: true,
rounded: false,
disabled: false,
});
await onClick!();
expect(outputs).toStrictEqual([values.STR('clicked')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:button({}, 'id')
`);
const { onClick, ...button } = get('id') as AsUiButton;
expect(onClick).toBeTypeOf('function');
expect(button).toStrictEqual({
type: 'button',
id: 'id',
text: undefined,
primary: undefined,
rounded: undefined,
disabled: undefined,
});
});
});
describe('buttons', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let buttons = Ui:C:buttons({
buttons: []
}, 'id')
Ui:render([buttons])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'buttons',
id: 'id',
buttons: [],
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:buttons({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'buttons',
id: 'id',
buttons: [],
});
});
test.concurrent('some buttons', async () => {
const { root, get, outputs } = await exe(`
let buttons = Ui:C:buttons({
buttons: [
{
text: 'a'
onClick: @() { <: 'clicked a' }
primary: true
rounded: false
disabled: false
}
{
text: 'b'
onClick: @() { <: 'clicked b' }
primary: true
rounded: false
disabled: false
}
]
}, 'id')
Ui:render([buttons])
`);
expect(root.children).toStrictEqual(['id']);
const { buttons, ...buttonsOptions } = get('id') as AsUiButtons;
expect(buttonsOptions).toStrictEqual({
type: 'buttons',
id: 'id',
});
expect(buttons!.length).toBe(2);
const { onClick: onClickA, ...buttonA } = buttons![0];
expect(buttonA).toStrictEqual({
text: 'a',
primary: true,
rounded: false,
disabled: false,
});
const { onClick: onClickB, ...buttonB } = buttons![1];
expect(buttonB).toStrictEqual({
text: 'b',
primary: true,
rounded: false,
disabled: false,
});
await onClickA!();
await onClickB!();
expect(outputs).toStrictEqual(
[values.STR('clicked a'), values.STR('clicked b')]
);
});
});
describe('switch', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let switch = Ui:C:switch({
onChange: print
default: false
label: 'a'
caption: 'b'
}, 'id')
Ui:render([switch])
`);
expect(root.children).toStrictEqual(['id']);
const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
expect(switchOptions).toStrictEqual({
type: 'switch',
id: 'id',
default: false,
label: 'a',
caption: 'b',
});
await onChange!(true);
expect(outputs).toStrictEqual([values.TRUE]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:switch({}, 'id')
`);
const { onChange, ...switchOptions } = get('id') as AsUiSwitch;
expect(onChange).toBeTypeOf('function');
expect(switchOptions).toStrictEqual({
type: 'switch',
id: 'id',
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('select', () => {
test.concurrent('all options', async () => {
const { root, get, outputs } = await exe(`
let select = Ui:C:select({
items: [
{ text: 'A', value: 'a' }
{ text: 'B', value: 'b' }
]
onChange: print
default: 'a'
label: 'c'
caption: 'd'
}, 'id')
Ui:render([select])
`);
expect(root.children).toStrictEqual(['id']);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [
{ text: 'A', value: 'a' },
{ text: 'B', value: 'b' },
],
default: 'a',
label: 'c',
caption: 'd',
});
await onChange!('b');
expect(outputs).toStrictEqual([values.STR('b')]);
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:select({}, 'id')
`);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(onChange).toBeTypeOf('function');
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [],
default: undefined,
label: undefined,
caption: undefined,
});
});
test.concurrent('omit item values', async () => {
const { get } = await exe(`
let select = Ui:C:select({
items: [
{ text: 'A' }
{ text: 'B' }
]
}, 'id')
`);
const { onChange, ...select } = get('id') as AsUiSelect;
expect(onChange).toBeTypeOf('function');
expect(select).toStrictEqual({
type: 'select',
id: 'id',
items: [
{ text: 'A', value: 'A' },
{ text: 'B', value: 'B' },
],
default: undefined,
label: undefined,
caption: undefined,
});
});
});
describe('folder', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let folder = Ui:C:folder({
children: []
title: 'a'
opened: true
}, 'id')
Ui:render([folder])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'folder',
id: 'id',
children: [],
title: 'a',
opened: true,
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:folder({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'folder',
id: 'id',
children: [],
title: '',
opened: true,
});
});
test.concurrent('some children', async () => {
const { get } = await exe(`
let text = Ui:C:text({
text: 'text'
}, 'id1')
Ui:C:folder({
children: [text]
}, 'id2')
`);
expect(get('id2')).toStrictEqual({
type: 'folder',
id: 'id2',
children: ['id1'],
title: '',
opened: true,
});
});
});
describe('postFormButton', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let post_form_button = Ui:C:postFormButton({
text: 'a'
primary: true
rounded: false
form: {
text: 'b'
cw: 'c'
visibility: 'public'
localOnly: true
}
}, 'id')
Ui:render([post_form_button])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'postFormButton',
id: 'id',
text: 'a',
primary: true,
rounded: false,
form: {
text: 'b',
cw: 'c',
visibility: 'public',
localOnly: true,
},
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:postFormButton({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postFormButton',
id: 'id',
text: undefined,
primary: undefined,
rounded: undefined,
form: { text: '' },
});
});
});
describe('postForm', () => {
test.concurrent('all options', async () => {
const { root, get } = await exe(`
let post_form = Ui:C:postForm({
form: {
text: 'a'
cw: 'b'
visibility: 'public'
localOnly: true
}
}, 'id')
Ui:render([post_form])
`);
expect(root.children).toStrictEqual(['id']);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: {
text: 'a',
cw: 'b',
visibility: 'public',
localOnly: true,
},
});
});
test.concurrent('minimum options', async () => {
const { get } = await exe(`
Ui:C:postForm({}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: { text: '' },
});
});
test.concurrent('minimum options for form', async () => {
const { get } = await exe(`
Ui:C:postForm({
form: { text: '' }
}, 'id')
`);
expect(get('id')).toStrictEqual({
type: 'postForm',
id: 'id',
form: {
text: '',
cw: undefined,
visibility: undefined,
localOnly: undefined,
},
});
});
});
});