mirror of
https://github.com/kokonect-link/cherrypick
synced 2025-01-18 07:43:49 +09:00
Merge remote-branch 'misskey/develop'
This commit is contained in:
commit
3c94611343
@ -8,6 +8,7 @@
|
||||
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
|
||||
- Enhance: 照会に失敗した場合、その理由を表示するように
|
||||
- Enhance: AiScriptのセーブデータを明示的に削除する関数`Mk:remove`を追加
|
||||
- Enhance: AiScriptの拡張API関数において引数の型チェックをより厳格に
|
||||
- Fix: 画面サイズが変わった際にナビゲーションバーが自動で折りたたまれない問題を修正
|
||||
- Fix: サーバー情報メニューに区切り線が不足していたのを修正
|
||||
- Fix: ノートがログインしているユーザーしか見れない場合にログインダイアログを閉じるとその後の動線がなくなる問題を修正
|
||||
@ -20,13 +21,17 @@
|
||||
(Cherry-picked from https://github.com/TeamNijimiss/misskey/commit/800359623e41a662551d774de15b0437b6849bb4)
|
||||
- Fix: ノート作成画面でファイルの添付可能個数を超えてもノートボタンが押せていた問題を修正
|
||||
- Fix: 「アカウントを管理」画面で、ユーザー情報の取得に失敗したアカウント(削除されたアカウントなど)が表示されない問題を修正
|
||||
- Fix: 言語データのキャッシュ状況によっては、埋め込みウィジェットが正しく起動しない問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: pg_bigmが利用できるよう、ノートの検索をILIKE演算子でなくLIKE演算子でLOWER()をかけたテキストに対して行うように
|
||||
- Fix: ユーザーのプロフィール画面をアドレス入力などで直接表示した際に概要タブの描画に失敗する問題の修正( #15032 )
|
||||
- Fix: 起動前の疎通チェックが機能しなくなっていた問題を修正
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/737)
|
||||
|
||||
- Fix: ロックダウンされた期間指定のノートがStreaming経由でLTLに出現するのを修正 ( #15200 )
|
||||
- Fix: disableClustering設定時の初期化ロジックを調整( #15223 )
|
||||
- Fix: ActivityPubリクエストかどうかの判定が正しくない問題を修正
|
||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/869)
|
||||
|
||||
## 2024.11.0
|
||||
|
||||
|
@ -91,16 +91,22 @@ process.on('warning', warning => {
|
||||
|
||||
//#endregion
|
||||
|
||||
if (cluster.isPrimary || envOption.disableClustering) {
|
||||
await masterMain();
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
if (cluster.isPrimary) {
|
||||
logger.info(`Start main process... pid: ${process.pid}`);
|
||||
await masterMain();
|
||||
ev.mount();
|
||||
} else if (cluster.isWorker) {
|
||||
logger.info(`Start worker process... pid: ${process.pid}`);
|
||||
await workerMain();
|
||||
} else {
|
||||
throw new Error('Unknown process type');
|
||||
}
|
||||
}
|
||||
|
||||
if (cluster.isWorker || envOption.disableClustering) {
|
||||
await workerMain();
|
||||
} else {
|
||||
// 非clusterの場合はMasterのみが起動するため、Workerの処理は行わない(cluster.isWorker === trueの状態でこのブロックに来ることはない)
|
||||
logger.info(`Start main process... pid: ${process.pid}`);
|
||||
await masterMain();
|
||||
ev.mount();
|
||||
}
|
||||
|
||||
readyRef.value = true;
|
||||
|
@ -94,25 +94,37 @@ export async function masterMain() {
|
||||
});
|
||||
}
|
||||
|
||||
if (envOption.disableClustering) {
|
||||
bootLogger.info(
|
||||
`mode: [disableClustering: ${envOption.disableClustering}, onlyServer: ${envOption.onlyServer}, onlyQueue: ${envOption.onlyQueue}]`
|
||||
);
|
||||
|
||||
if (!envOption.disableClustering) {
|
||||
// clusterモジュール有効時
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
// onlyServer かつ enableCluster な場合、メインプロセスはforkのみに制限する(listenしない)。
|
||||
// ワーカープロセス側でlistenすると、メインプロセスでポートへの着信を受け入れてワーカープロセスへの分配を行う動作をする。
|
||||
// そのため、メインプロセスでも直接listenするとポートの競合が発生して起動に失敗してしまう。
|
||||
// see: https://nodejs.org/api/cluster.html#cluster
|
||||
} else if (envOption.onlyQueue) {
|
||||
await jobQueue();
|
||||
} else {
|
||||
await server();
|
||||
await jobQueue();
|
||||
}
|
||||
} else {
|
||||
if (envOption.onlyServer) {
|
||||
// nop
|
||||
} else if (envOption.onlyQueue) {
|
||||
// nop
|
||||
} else {
|
||||
await server();
|
||||
}
|
||||
|
||||
await spawnWorkers(config.clusterLimit);
|
||||
} else {
|
||||
// clusterモジュール無効時
|
||||
|
||||
if (envOption.onlyServer) {
|
||||
await server();
|
||||
} else if (envOption.onlyQueue) {
|
||||
await jobQueue();
|
||||
} else {
|
||||
await server();
|
||||
await jobQueue();
|
||||
}
|
||||
}
|
||||
|
||||
if (envOption.onlyQueue) {
|
||||
|
@ -105,8 +105,7 @@ export class NoteEntityService implements OnModuleInit {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
||||
// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
|
||||
private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
|
||||
if ((followersOnlyBefore != null)
|
||||
@ -118,7 +117,11 @@ export class NoteEntityService implements OnModuleInit {
|
||||
packedNote.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
return packedNote.visibility;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
||||
if (meId === packedNote.userId) return;
|
||||
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
@ -479,6 +482,8 @@ export class NoteEntityService implements OnModuleInit {
|
||||
} : {}),
|
||||
});
|
||||
|
||||
this.treatVisibility(packed);
|
||||
|
||||
if (!opts.skipHide) {
|
||||
await this.hideNote(packed, meId);
|
||||
}
|
||||
|
@ -525,8 +525,8 @@ export class ActivityPubServerService {
|
||||
},
|
||||
deriveConstraint(request: IncomingMessage) {
|
||||
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
|
||||
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
|
||||
return isAp ? 'ap' : 'html';
|
||||
if (accepted === false) return null;
|
||||
return accepted !== 'html' ? 'ap' : 'html';
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
|
||||
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { Options } from 'reconnecting-websocket';
|
||||
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
||||
import _ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
@ -3341,7 +3341,7 @@ export class Stream extends EventEmitter<StreamEvents> implements IStream {
|
||||
constructor(origin: string, user: {
|
||||
token: string;
|
||||
} | null, options?: {
|
||||
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
|
||||
WebSocket?: Options['WebSocket'];
|
||||
});
|
||||
// (undocumented)
|
||||
close(): void;
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import _ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import _ReconnectingWebSocket, { Options } from 'reconnecting-websocket';
|
||||
import type { BroadcastEvents, Channels } from './streaming.types.js';
|
||||
|
||||
const ReconnectingWebsocket = _ReconnectingWebsocket as unknown as typeof _ReconnectingWebsocket['default'];
|
||||
// コンストラクタとクラスそのものの定義が上手く解決出来ないため再定義
|
||||
const ReconnectingWebSocketConstructor = _ReconnectingWebSocket as unknown as typeof _ReconnectingWebSocket.default;
|
||||
type ReconnectingWebSocket = _ReconnectingWebSocket.default;
|
||||
|
||||
export function urlQuery(obj: Record<string, string | number | boolean | undefined>): string {
|
||||
const params = Object.entries(obj)
|
||||
@ -43,7 +45,7 @@ export interface IStream extends EventEmitter<StreamEvents> {
|
||||
*/
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default class Stream extends EventEmitter<StreamEvents> implements IStream {
|
||||
private stream: _ReconnectingWebsocket.default;
|
||||
private stream: ReconnectingWebSocket;
|
||||
public state: 'initializing' | 'reconnecting' | 'connected' = 'initializing';
|
||||
private sharedConnectionPools: Pool[] = [];
|
||||
private sharedConnections: SharedConnection[] = [];
|
||||
@ -51,7 +53,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
|
||||
private idCounter = 0;
|
||||
|
||||
constructor(origin: string, user: { token: string; } | null, options?: {
|
||||
WebSocket?: _ReconnectingWebsocket.Options['WebSocket'];
|
||||
WebSocket?: Options['WebSocket'];
|
||||
}) {
|
||||
super();
|
||||
|
||||
@ -80,7 +82,7 @@ export default class Stream extends EventEmitter<StreamEvents> implements IStrea
|
||||
|
||||
const wsOrigin = origin.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
|
||||
this.stream = new ReconnectingWebsocket(`${wsOrigin}/streaming?${query}`, '', {
|
||||
this.stream = new ReconnectingWebSocketConstructor(`${wsOrigin}/streaming?${query}`, '', {
|
||||
minReconnectionDelay: 1, // https://github.com/pladaria/reconnecting-websocket/issues/91
|
||||
WebSocket: options.WebSocket,
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import { createApp, defineAsyncComponent } from 'vue';
|
||||
import defaultLightTheme from '@@/themes/l-light.json5';
|
||||
import defaultDarkTheme from '@@/themes/d-dark.json5';
|
||||
import { MediaProxy } from '@@/js/media-proxy.js';
|
||||
import { url } from '@@/js/config.js';
|
||||
import { url, version, basedMisskeyVersion, locale, lang, updateLocale } from '@@/js/config.js';
|
||||
import { parseEmbedParams } from '@@/js/embed-page.js';
|
||||
import type { Theme } from '@/theme.js';
|
||||
import { applyTheme, assertIsTheme } from '@/theme.js';
|
||||
@ -22,7 +22,7 @@ import { DI } from '@/di.js';
|
||||
import { serverMetadata } from '@/server-metadata.js';
|
||||
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
|
||||
import { serverContext } from '@/server-context.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { i18n, updateI18n } from '@/i18n.js';
|
||||
|
||||
console.log('CherryPick Embed');
|
||||
|
||||
@ -70,6 +70,23 @@ if (embedParams.colorMode === 'dark') {
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Detect language & fetch translations
|
||||
const localeVersion = localStorage.getItem('localeVersion');
|
||||
const lastBasedMisskeyVersion = localStorage.getItem('lastBasedMisskeyVersion');
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== version || lastBasedMisskeyVersion !== basedMisskeyVersion || locale == null);
|
||||
if (localeOutdated) {
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
|
||||
if (res.status === 200) {
|
||||
const newLocale = await res.text();
|
||||
const parsedNewLocale = JSON.parse(newLocale);
|
||||
localStorage.setItem('locale', newLocale);
|
||||
localStorage.setItem('localeVersion', version);
|
||||
updateLocale(parsedNewLocale);
|
||||
updateI18n(parsedNewLocale);
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
// サイズの制限
|
||||
document.documentElement.style.maxWidth = '500px';
|
||||
|
||||
|
@ -421,8 +421,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
|
||||
return [h(EmEmoji, {
|
||||
key: Math.random(),
|
||||
emoji: token.props.emoji,
|
||||
menu: props.enableEmojiMenu,
|
||||
menuReaction: props.enableEmojiMenuReaction,
|
||||
})];
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root]">
|
||||
<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
||||
<EmNote v-for="note in notes" :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note as Misskey.entities.Note"/>
|
||||
</div>
|
||||
</template>
|
||||
</EmPagination>
|
||||
@ -21,6 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTemplateRef } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import EmNote from '@/components/EmNote.vue';
|
||||
import EmPagination, { Paging } from '@/components/EmPagination.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
@ -75,16 +75,21 @@ function compile(theme: Theme): Record<string, string> {
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const func = parts.shift().substring(1);
|
||||
const arg = parseFloat(parts.shift());
|
||||
const color = getColor(parts.join('<'));
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"target": "ES2022",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@typescript-eslint/parser": "7.17.0",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint-plugin-vue": "9.31.0",
|
||||
"nodemon": "3.1.7",
|
||||
"typescript": "5.6.3",
|
||||
"vue-eslint-parser": "9.4.3"
|
||||
},
|
||||
|
@ -60,6 +60,7 @@ function getInstanceIcon(instance: Misskey.entities.FederationInstance): string
|
||||
misskeyApiGet('federation/instances', {
|
||||
sort: '+pubSub',
|
||||
limit: 20,
|
||||
blocked: 'false',
|
||||
}).then(_instances => {
|
||||
instances.value = _instances;
|
||||
});
|
||||
|
@ -5,15 +5,25 @@
|
||||
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import { permissions as MkPermissions } from 'cherrypick-js';
|
||||
import { utils, values } from '@syuilo/aiscript';
|
||||
import { errors, utils, values } from '@syuilo/aiscript';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { url, lang } from '@@/js/config.js';
|
||||
import { assertStringAndIsIn } from './common.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
|
||||
const DIALOG_TYPES = [
|
||||
'error',
|
||||
'info',
|
||||
'success',
|
||||
'warning',
|
||||
'waiting',
|
||||
'question',
|
||||
] as const;
|
||||
|
||||
export function aiScriptReadline(q: string): Promise<string> {
|
||||
return new Promise(ok => {
|
||||
os.inputText({
|
||||
@ -24,19 +34,24 @@ export function aiScriptReadline(q: string): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
export function createAiScriptEnv(opts) {
|
||||
export function createAiScriptEnv(opts: { storageKey: string, token?: string }) {
|
||||
const table = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
const randomString = Array.from(crypto.getRandomValues(new Uint32Array(32)))
|
||||
.map(v => table[v % table.length])
|
||||
.join('');
|
||||
return {
|
||||
USER_ID: $i ? values.STR($i.id) : values.NULL,
|
||||
USER_NAME: $i ? values.STR($i.name) : values.NULL,
|
||||
USER_NAME: $i?.name ? values.STR($i.name) : values.NULL,
|
||||
USER_USERNAME: $i ? values.STR($i.username) : values.NULL,
|
||||
CUSTOM_EMOJIS: utils.jsToVal(customEmojis.value),
|
||||
LOCALE: values.STR(lang),
|
||||
SERVER_URL: values.STR(url),
|
||||
'Mk:dialog': values.FN_NATIVE(async ([title, text, type]) => {
|
||||
utils.assertString(title);
|
||||
utils.assertString(text);
|
||||
if (type != null) {
|
||||
assertStringAndIsIn(type, DIALOG_TYPES);
|
||||
}
|
||||
await os.alert({
|
||||
type: type ? type.value : 'info',
|
||||
title: title.value,
|
||||
@ -45,6 +60,11 @@ export function createAiScriptEnv(opts) {
|
||||
return values.NULL;
|
||||
}),
|
||||
'Mk:confirm': values.FN_NATIVE(async ([title, text, type]) => {
|
||||
utils.assertString(title);
|
||||
utils.assertString(text);
|
||||
if (type != null) {
|
||||
assertStringAndIsIn(type, DIALOG_TYPES);
|
||||
}
|
||||
const confirm = await os.confirm({
|
||||
type: type ? type.value : 'question',
|
||||
title: title.value,
|
||||
@ -54,14 +74,20 @@ export function createAiScriptEnv(opts) {
|
||||
}),
|
||||
'Mk:api': values.FN_NATIVE(async ([ep, param, token]) => {
|
||||
utils.assertString(ep);
|
||||
if (ep.value.includes('://')) throw new Error('invalid endpoint');
|
||||
if (ep.value.includes('://')) {
|
||||
throw new errors.AiScriptRuntimeError('invalid endpoint');
|
||||
}
|
||||
if (token) {
|
||||
utils.assertString(token);
|
||||
// バグがあればundefinedもあり得るため念のため
|
||||
if (typeof token.value !== 'string') throw new Error('invalid token');
|
||||
}
|
||||
const actualToken: string|null = token?.value ?? miLocalStorage.getItem(`aiscriptSecure:${opts.storageKey}:${randomString}:accessToken`) ?? opts.token ?? null;
|
||||
return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => {
|
||||
if (param == null) {
|
||||
throw new errors.AiScriptRuntimeError('expected param');
|
||||
}
|
||||
utils.assertObject(param);
|
||||
return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => {
|
||||
return utils.jsToVal(res);
|
||||
}, err => {
|
||||
return values.ERROR('request_failed', utils.jsToVal(err));
|
||||
|
15
packages/frontend/src/scripts/aiscript/common.ts
Normal file
15
packages/frontend/src/scripts/aiscript/common.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { errors, utils, type values } from '@syuilo/aiscript';
|
||||
|
||||
export function assertStringAndIsIn<A extends readonly string[]>(value: values.Value | undefined, expects: A): asserts value is values.VStr & { value: A[number] } {
|
||||
utils.assertString(value);
|
||||
const str = value.value;
|
||||
if (!expects.includes(str)) {
|
||||
const expected = expects.map((expect) => `"${expect}"`).join(', ');
|
||||
throw new errors.AiScriptRuntimeError(`"${value.value}" is not in ${expected}`);
|
||||
}
|
||||
}
|
@ -7,6 +7,15 @@ import { utils, values } from '@syuilo/aiscript';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { ref, Ref } from 'vue';
|
||||
import * as Misskey from 'cherrypick-js';
|
||||
import { assertStringAndIsIn } from './common.js';
|
||||
|
||||
const ALIGNS = ['left', 'center', 'right'] as const;
|
||||
const FONTS = ['serif', 'sans-serif', 'monospace'] as const;
|
||||
const BORDER_STYLES = ['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] as const;
|
||||
|
||||
type Align = (typeof ALIGNS)[number];
|
||||
type Font = (typeof FONTS)[number];
|
||||
type BorderStyle = (typeof BORDER_STYLES)[number];
|
||||
|
||||
export type AsUiComponentBase = {
|
||||
id: string;
|
||||
@ -21,13 +30,13 @@ export type AsUiRoot = AsUiComponentBase & {
|
||||
export type AsUiContainer = AsUiComponentBase & {
|
||||
type: 'container';
|
||||
children?: AsUiComponent['id'][];
|
||||
align?: 'left' | 'center' | 'right';
|
||||
align?: Align;
|
||||
bgColor?: string;
|
||||
fgColor?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
font?: Font;
|
||||
borderWidth?: number;
|
||||
borderColor?: string;
|
||||
borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
|
||||
borderStyle?: BorderStyle;
|
||||
borderRadius?: number;
|
||||
padding?: number;
|
||||
rounded?: boolean;
|
||||
@ -40,7 +49,7 @@ export type AsUiText = AsUiComponentBase & {
|
||||
size?: number;
|
||||
bold?: boolean;
|
||||
color?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
font?: Font;
|
||||
};
|
||||
|
||||
export type AsUiMfm = AsUiComponentBase & {
|
||||
@ -49,14 +58,14 @@ export type AsUiMfm = AsUiComponentBase & {
|
||||
size?: number;
|
||||
bold?: boolean;
|
||||
color?: string;
|
||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||
onClickEv?: (evId: string) => void
|
||||
font?: Font;
|
||||
onClickEv?: (evId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export type AsUiButton = AsUiComponentBase & {
|
||||
type: 'button';
|
||||
text?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: () => Promise<void>;
|
||||
primary?: boolean;
|
||||
rounded?: boolean;
|
||||
disabled?: boolean;
|
||||
@ -69,7 +78,7 @@ export type AsUiButtons = AsUiComponentBase & {
|
||||
|
||||
export type AsUiSwitch = AsUiComponentBase & {
|
||||
type: 'switch';
|
||||
onChange?: (v: boolean) => void;
|
||||
onChange?: (v: boolean) => Promise<void>;
|
||||
default?: boolean;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
@ -77,7 +86,7 @@ export type AsUiSwitch = AsUiComponentBase & {
|
||||
|
||||
export type AsUiTextarea = AsUiComponentBase & {
|
||||
type: 'textarea';
|
||||
onInput?: (v: string) => void;
|
||||
onInput?: (v: string) => Promise<void>;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
@ -85,7 +94,7 @@ export type AsUiTextarea = AsUiComponentBase & {
|
||||
|
||||
export type AsUiTextInput = AsUiComponentBase & {
|
||||
type: 'textInput';
|
||||
onInput?: (v: string) => void;
|
||||
onInput?: (v: string) => Promise<void>;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
@ -93,7 +102,7 @@ export type AsUiTextInput = AsUiComponentBase & {
|
||||
|
||||
export type AsUiNumberInput = AsUiComponentBase & {
|
||||
type: 'numberInput';
|
||||
onInput?: (v: number) => void;
|
||||
onInput?: (v: number) => Promise<void>;
|
||||
default?: number;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
@ -105,7 +114,7 @@ export type AsUiSelect = AsUiComponentBase & {
|
||||
text: string;
|
||||
value: string;
|
||||
}[];
|
||||
onChange?: (v: string) => void;
|
||||
onChange?: (v: string) => Promise<void>;
|
||||
default?: string;
|
||||
label?: string;
|
||||
caption?: string;
|
||||
@ -140,11 +149,15 @@ export type AsUiPostForm = AsUiComponentBase & {
|
||||
|
||||
export type AsUiComponent = AsUiRoot | AsUiContainer | AsUiText | AsUiMfm | AsUiButton | AsUiButtons | AsUiSwitch | AsUiTextarea | AsUiTextInput | AsUiNumberInput | AsUiSelect | AsUiFolder | AsUiPostFormButton | AsUiPostForm;
|
||||
|
||||
type Options<T extends AsUiComponent> = T extends AsUiButtons
|
||||
? Omit<T, 'id' | 'type' | 'buttons'> & { 'buttons'?: Options<AsUiButton>[] }
|
||||
: Omit<T, 'id' | 'type'>;
|
||||
|
||||
export function patch(id: string, def: values.Value, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 'type'> {
|
||||
function getRootOptions(def: values.Value | undefined): Options<AsUiRoot> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const children = def.value.get('children');
|
||||
@ -153,30 +166,32 @@ function getRootOptions(def: values.Value | undefined): Omit<AsUiRoot, 'id' | 't
|
||||
return {
|
||||
children: children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
const id = v.value.get('id');
|
||||
utils.assertString(id);
|
||||
return id.value;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer, 'id' | 'type'> {
|
||||
function getContainerOptions(def: values.Value | undefined): Options<AsUiContainer> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const children = def.value.get('children');
|
||||
if (children) utils.assertArray(children);
|
||||
const align = def.value.get('align');
|
||||
if (align) utils.assertString(align);
|
||||
if (align) assertStringAndIsIn(align, ALIGNS);
|
||||
const bgColor = def.value.get('bgColor');
|
||||
if (bgColor) utils.assertString(bgColor);
|
||||
const fgColor = def.value.get('fgColor');
|
||||
if (fgColor) utils.assertString(fgColor);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
if (font) assertStringAndIsIn(font, FONTS);
|
||||
const borderWidth = def.value.get('borderWidth');
|
||||
if (borderWidth) utils.assertNumber(borderWidth);
|
||||
const borderColor = def.value.get('borderColor');
|
||||
if (borderColor) utils.assertString(borderColor);
|
||||
const borderStyle = def.value.get('borderStyle');
|
||||
if (borderStyle) utils.assertString(borderStyle);
|
||||
if (borderStyle) assertStringAndIsIn(borderStyle, BORDER_STYLES);
|
||||
const borderRadius = def.value.get('borderRadius');
|
||||
if (borderRadius) utils.assertNumber(borderRadius);
|
||||
const padding = def.value.get('padding');
|
||||
@ -189,7 +204,9 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
|
||||
return {
|
||||
children: children ? children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
const id = v.value.get('id');
|
||||
utils.assertString(id);
|
||||
return id.value;
|
||||
}) : [],
|
||||
align: align?.value,
|
||||
fgColor: fgColor?.value,
|
||||
@ -205,7 +222,7 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
|
||||
};
|
||||
}
|
||||
|
||||
function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 'type'> {
|
||||
function getTextOptions(def: values.Value | undefined): Options<AsUiText> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
@ -217,7 +234,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
|
||||
const color = def.value.get('color');
|
||||
if (color) utils.assertString(color);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
if (font) assertStringAndIsIn(font, FONTS);
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
@ -228,7 +245,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't
|
||||
};
|
||||
}
|
||||
|
||||
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> {
|
||||
function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiMfm> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
@ -240,7 +257,7 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
|
||||
const color = def.value.get('color');
|
||||
if (color) utils.assertString(color);
|
||||
const font = def.value.get('font');
|
||||
if (font) utils.assertString(font);
|
||||
if (font) assertStringAndIsIn(font, FONTS);
|
||||
const onClickEv = def.value.get('onClickEv');
|
||||
if (onClickEv) utils.assertFunction(onClickEv);
|
||||
|
||||
@ -250,13 +267,13 @@ function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, arg
|
||||
bold: bold?.value,
|
||||
color: color?.value,
|
||||
font: font?.value,
|
||||
onClickEv: (evId: string) => {
|
||||
if (onClickEv) call(onClickEv, [values.STR(evId)]);
|
||||
onClickEv: async (evId: string) => {
|
||||
if (onClickEv) await call(onClickEv, [values.STR(evId)]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextInput, 'id' | 'type'> {
|
||||
function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextInput> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
@ -269,8 +286,8 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
onInput: async (v) => {
|
||||
if (onInput) await call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
@ -278,7 +295,7 @@ function getTextInputOptions(def: values.Value | undefined, call: (fn: values.VF
|
||||
};
|
||||
}
|
||||
|
||||
function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiTextarea, 'id' | 'type'> {
|
||||
function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiTextarea> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
@ -291,8 +308,8 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
onInput: async (v) => {
|
||||
if (onInput) await call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
@ -300,7 +317,7 @@ function getTextareaOptions(def: values.Value | undefined, call: (fn: values.VFn
|
||||
};
|
||||
}
|
||||
|
||||
function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiNumberInput, 'id' | 'type'> {
|
||||
function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiNumberInput> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onInput = def.value.get('onInput');
|
||||
@ -313,8 +330,8 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onInput: (v) => {
|
||||
if (onInput) call(onInput, [utils.jsToVal(v)]);
|
||||
onInput: async (v) => {
|
||||
if (onInput) await call(onInput, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
@ -322,7 +339,7 @@ function getNumberInputOptions(def: values.Value | undefined, call: (fn: values.
|
||||
};
|
||||
}
|
||||
|
||||
function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButton, 'id' | 'type'> {
|
||||
function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButton> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
@ -338,8 +355,8 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
|
||||
return {
|
||||
text: text?.value,
|
||||
onClick: () => {
|
||||
if (onClick) call(onClick, []);
|
||||
onClick: async () => {
|
||||
if (onClick) await call(onClick, []);
|
||||
},
|
||||
primary: primary?.value,
|
||||
rounded: rounded?.value,
|
||||
@ -347,7 +364,7 @@ function getButtonOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
};
|
||||
}
|
||||
|
||||
function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiButtons, 'id' | 'type'> {
|
||||
function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiButtons> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const buttons = def.value.get('buttons');
|
||||
@ -369,8 +386,8 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
|
||||
return {
|
||||
text: text.value,
|
||||
onClick: () => {
|
||||
call(onClick, []);
|
||||
onClick: async () => {
|
||||
await call(onClick, []);
|
||||
},
|
||||
primary: primary?.value,
|
||||
rounded: rounded?.value,
|
||||
@ -380,7 +397,7 @@ function getButtonsOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
};
|
||||
}
|
||||
|
||||
function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSwitch, 'id' | 'type'> {
|
||||
function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSwitch> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const onChange = def.value.get('onChange');
|
||||
@ -393,8 +410,8 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
if (caption) utils.assertString(caption);
|
||||
|
||||
return {
|
||||
onChange: (v) => {
|
||||
if (onChange) call(onChange, [utils.jsToVal(v)]);
|
||||
onChange: async (v) => {
|
||||
if (onChange) await call(onChange, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
@ -402,7 +419,7 @@ function getSwitchOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
};
|
||||
}
|
||||
|
||||
function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiSelect, 'id' | 'type'> {
|
||||
function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiSelect> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const items = def.value.get('items');
|
||||
@ -428,8 +445,8 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
value: value ? value.value : text.value,
|
||||
};
|
||||
}) : [],
|
||||
onChange: (v) => {
|
||||
if (onChange) call(onChange, [utils.jsToVal(v)]);
|
||||
onChange: async (v) => {
|
||||
if (onChange) await call(onChange, [utils.jsToVal(v)]);
|
||||
},
|
||||
default: defaultValue?.value,
|
||||
label: label?.value,
|
||||
@ -437,7 +454,7 @@ function getSelectOptions(def: values.Value | undefined, call: (fn: values.VFn,
|
||||
};
|
||||
}
|
||||
|
||||
function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id' | 'type'> {
|
||||
function getFolderOptions(def: values.Value | undefined): Options<AsUiFolder> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const children = def.value.get('children');
|
||||
@ -450,7 +467,9 @@ function getFolderOptions(def: values.Value | undefined): Omit<AsUiFolder, 'id'
|
||||
return {
|
||||
children: children ? children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
const id = v.value.get('id');
|
||||
utils.assertString(id);
|
||||
return id.value;
|
||||
}) : [],
|
||||
title: title?.value ?? '',
|
||||
opened: opened?.value ?? true,
|
||||
@ -475,7 +494,7 @@ function getPostFormProps(form: values.VObj): PostFormPropsForAsUi {
|
||||
};
|
||||
}
|
||||
|
||||
function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostFormButton, 'id' | 'type'> {
|
||||
function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostFormButton> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const text = def.value.get('text');
|
||||
@ -497,7 +516,7 @@ function getPostFormButtonOptions(def: values.Value | undefined, call: (fn: valu
|
||||
};
|
||||
}
|
||||
|
||||
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiPostForm, 'id' | 'type'> {
|
||||
function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Options<AsUiPostForm> {
|
||||
utils.assertObject(def);
|
||||
|
||||
const form = def.value.get('form');
|
||||
@ -511,18 +530,26 @@ function getPostFormOptions(def: values.Value | undefined, call: (fn: values.VFn
|
||||
}
|
||||
|
||||
export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: Ref<AsUiRoot>) => void) {
|
||||
type OptionsConverter<T extends AsUiComponent, C> = (def: values.Value | undefined, call: C) => Options<T>;
|
||||
|
||||
const instances = {};
|
||||
|
||||
function createComponentInstance(type: AsUiComponent['type'], def: values.Value | undefined, id: values.Value | undefined, getOptions: (def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) => any, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>) {
|
||||
function createComponentInstance<T extends AsUiComponent, C>(
|
||||
type: T['type'],
|
||||
def: values.Value | undefined,
|
||||
id: values.Value | undefined,
|
||||
getOptions: OptionsConverter<T, C>,
|
||||
call: C,
|
||||
) {
|
||||
if (id) utils.assertString(id);
|
||||
const _id = id?.value ?? uuid();
|
||||
const component = ref({
|
||||
...getOptions(def, call),
|
||||
type,
|
||||
id: _id,
|
||||
});
|
||||
} as T);
|
||||
components.push(component);
|
||||
const instance = values.OBJ(new Map([
|
||||
const instance = values.OBJ(new Map<string, values.Value>([
|
||||
['id', values.STR(_id)],
|
||||
['update', values.FN_NATIVE(([def], opts) => {
|
||||
utils.assertObject(def);
|
||||
@ -547,7 +574,7 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
'Ui:patch': values.FN_NATIVE(([id, val], opts) => {
|
||||
utils.assertString(id);
|
||||
utils.assertArray(val);
|
||||
patch(id.value, val.value, opts.call);
|
||||
// patch(id.value, val.value, opts.call); // TODO
|
||||
}),
|
||||
|
||||
'Ui:get': values.FN_NATIVE(([id], opts) => {
|
||||
@ -566,7 +593,9 @@ export function registerAsUiLib(components: Ref<AsUiComponent>[], done: (root: R
|
||||
|
||||
rootComponent.value.children = children.value.map(v => {
|
||||
utils.assertObject(v);
|
||||
return v.value.get('id').value;
|
||||
const id = v.value.get('id');
|
||||
utils.assertString(id);
|
||||
return id.value;
|
||||
});
|
||||
}),
|
||||
|
||||
|
@ -201,7 +201,7 @@ export function getNoteMenu(props: {
|
||||
noteId: appearNote.id,
|
||||
});
|
||||
|
||||
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
|
||||
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
|
||||
claimAchievement('noteDeletedWithin1min');
|
||||
}
|
||||
});
|
||||
@ -220,7 +220,7 @@ export function getNoteMenu(props: {
|
||||
|
||||
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
|
||||
|
||||
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60) {
|
||||
if (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 60 && appearNote.userId === $i.id) {
|
||||
claimAchievement('noteDeletedWithin1min');
|
||||
}
|
||||
});
|
||||
|
@ -9,12 +9,24 @@ import { apiUrl } from '@@/js/config.js';
|
||||
import { $i } from '@/account.js';
|
||||
export const pendingApiRequestsCount = ref(0);
|
||||
|
||||
export type Endpoint = keyof Misskey.Endpoints;
|
||||
|
||||
export type Request<E extends Endpoint> = Misskey.Endpoints[E]['req'];
|
||||
|
||||
export type AnyRequest<E extends Endpoint | (string & unknown)> =
|
||||
(E extends Endpoint ? Request<E> : never) | object;
|
||||
|
||||
export type Response<E extends Endpoint | (string & unknown), P extends AnyRequest<E>> =
|
||||
E extends Endpoint
|
||||
? P extends Request<E> ? Misskey.api.SwitchCaseResponseType<E, P> : never
|
||||
: object;
|
||||
|
||||
// Implements Misskey.api.ApiClient.request
|
||||
export function misskeyApi<
|
||||
ResT = void,
|
||||
E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
|
||||
P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
|
||||
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
|
||||
E extends Endpoint | NonNullable<string> = Endpoint,
|
||||
P extends AnyRequest<E> = E extends Endpoint ? Request<E> : never,
|
||||
_ResT = ResT extends void ? Response<E, P> : ResT,
|
||||
>(
|
||||
endpoint: E,
|
||||
data: P & { i?: string | null; } = {} as any,
|
||||
|
401
packages/frontend/test/aiscript/api.test.ts
Normal file
401
packages/frontend/test/aiscript/api.test.ts
Normal 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('にゃ'));
|
||||
});
|
||||
});
|
23
packages/frontend/test/aiscript/common.test.ts
Normal file
23
packages/frontend/test/aiscript/common.test.ts
Normal 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"');
|
||||
});
|
||||
});
|
825
packages/frontend/test/aiscript/ui.test.ts
Normal file
825
packages/frontend/test/aiscript/ui.test.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -10,8 +10,8 @@
|
||||
"declaration": false,
|
||||
"sourceMap": false,
|
||||
"target": "ES2022",
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"removeComments": false,
|
||||
"noLib": false,
|
||||
"strict": true,
|
||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -1406,6 +1406,9 @@ importers:
|
||||
eslint-plugin-vue:
|
||||
specifier: 9.31.0
|
||||
version: 9.31.0(eslint@9.14.0)
|
||||
nodemon:
|
||||
specifier: 3.1.7
|
||||
version: 3.1.7
|
||||
typescript:
|
||||
specifier: 5.6.3
|
||||
version: 5.6.3
|
||||
|
Loading…
Reference in New Issue
Block a user