summaryrefslogtreecommitdiff
path: root/packages/frontend/test/aiscript/ui.test.ts
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-02-03 14:31:26 -0500
committerHazelnoot <acomputerdog@gmail.com>2025-02-03 14:36:09 -0500
commita4e86758c1c53f4e623b6e8f613d4a6e34e96156 (patch)
treed09bf325b7f52512a1fe2a9d35f1953d2b310309 /packages/frontend/test/aiscript/ui.test.ts
parentmerge: Use package manager version from package.json (!883) (diff)
parentfix(build): corepackのバグの回避 (#15387) (diff)
downloadsharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.tar.gz
sharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.tar.bz2
sharkey-a4e86758c1c53f4e623b6e8f613d4a6e34e96156.zip
merge upstream 2025-02-03
Diffstat (limited to 'packages/frontend/test/aiscript/ui.test.ts')
-rw-r--r--packages/frontend/test/aiscript/ui.test.ts825
1 files changed, 825 insertions, 0 deletions
diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts
new file mode 100644
index 0000000000..5f77edbb49
--- /dev/null
+++ b/packages/frontend/test/aiscript/ui.test.ts
@@ -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,
+ },
+ });
+ });
+ });
+});