mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-10-26 03:04:52 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			401 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			401 lines
		
	
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /*
 | |
|  * SPDX-FileCopyrightText: syuilo and misskey-project
 | |
|  * SPDX-License-Identifier: AGPL-3.0-only
 | |
|  */
 | |
| 
 | |
| import { miLocalStorage } from '@/local-storage.js';
 | |
| import { aiScriptReadline, createAiScriptEnv } from '@/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('@/i.js').$i> | null >(
 | |
| 	() => null
 | |
| );
 | |
| 
 | |
| vi.mock('@/i.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('@/utility/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('にゃ'));
 | |
| 	});
 | |
| });
 |