feat: Add slashcommand api
This commit is contained in:
@@ -0,0 +1,653 @@
|
||||
import { Injector, signal } from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { DatabaseService } from '../../../../infrastructure/persistence';
|
||||
import { ServerDirectoryFacade } from '../../../server-directory';
|
||||
import { AttachmentFacade } from '../../../attachment';
|
||||
import { VoiceConnectionFacade } from '../../../voice-connection/application/facades/voice-connection.facade';
|
||||
import type {
|
||||
Channel,
|
||||
Message,
|
||||
Room,
|
||||
TojuPluginManifest,
|
||||
User
|
||||
} from '../../../../shared-kernel';
|
||||
import { MessagesActions } from '../../../../store/messages/messages.actions';
|
||||
import { selectCurrentRoomMessages } from '../../../../store/messages/messages.selectors';
|
||||
import {
|
||||
selectActiveChannelId,
|
||||
selectCurrentRoom,
|
||||
selectCurrentRoomChannels,
|
||||
selectCurrentRoomId
|
||||
} from '../../../../store/rooms/rooms.selectors';
|
||||
import { UsersActions } from '../../../../store/users/users.actions';
|
||||
import { selectAllUsers, selectCurrentUser } from '../../../../store/users/users.selectors';
|
||||
import {
|
||||
PLUGIN_CLIENT_API_CAPABILITY_REQUIREMENTS,
|
||||
assertPluginApiSurfaceImplemented,
|
||||
collectPluginApiMethodPaths,
|
||||
type PluginClientApiMethodPath
|
||||
} from '../../domain/logic/plugin-client-api-surface.rules';
|
||||
import { PluginCapabilityError, PluginCapabilityService } from './plugin-capability.service';
|
||||
import { PluginClientApiService } from './plugin-client-api.service';
|
||||
import { PluginDesktopStateService } from './plugin-desktop-state.service';
|
||||
import { PluginLoggerService } from './plugin-logger.service';
|
||||
import { PluginMessageBusService } from './plugin-message-bus.service';
|
||||
import { PluginStorageService } from './plugin-storage.service';
|
||||
import { PluginUiRegistryService } from './plugin-ui-registry.service';
|
||||
|
||||
const TEST_MANIFEST = createTestManifest();
|
||||
|
||||
describe('PluginClientApiService', () => {
|
||||
let context: ServiceTestContext;
|
||||
|
||||
beforeEach(() => {
|
||||
context = createServiceTestContext();
|
||||
context.capabilities.grantAll(TEST_MANIFEST);
|
||||
});
|
||||
|
||||
it('implements the full public plugin API surface', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
expect(() => assertPluginApiSurfaceImplemented(api as Record<string, Record<string, unknown>>)).not.toThrow();
|
||||
});
|
||||
|
||||
it('freezes the API object returned to plugins', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
expect(Object.isFrozen(api)).toBe(true);
|
||||
expect(Object.isFrozen(api.commands)).toBe(true);
|
||||
expect(Object.isFrozen(api.messages)).toBe(true);
|
||||
});
|
||||
|
||||
it('exposes current interaction context without capability checks', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
expect(api.context.getCurrent()).toEqual({
|
||||
server: context.room(),
|
||||
source: 'manual',
|
||||
textChannel: context.channels().find((channel) => channel.id === 'general') ?? null,
|
||||
user: context.currentUser(),
|
||||
voiceChannel: null
|
||||
});
|
||||
});
|
||||
|
||||
it('routes logger calls to the plugin logger service', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
api.logger.debug('debug');
|
||||
api.logger.error('error');
|
||||
api.logger.info('info');
|
||||
api.logger.warn('warn');
|
||||
|
||||
expect(context.logger.debug).toHaveBeenCalledWith(TEST_MANIFEST.id, 'debug', undefined);
|
||||
expect(context.logger.error).toHaveBeenCalledWith(TEST_MANIFEST.id, 'error', undefined);
|
||||
expect(context.logger.info).toHaveBeenCalledWith(TEST_MANIFEST.id, 'info', undefined);
|
||||
expect(context.logger.warn).toHaveBeenCalledWith(TEST_MANIFEST.id, 'warn', undefined);
|
||||
});
|
||||
|
||||
it('dispatches profile updates through the users store', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
api.profile.update({ displayName: 'Plugin User' });
|
||||
api.profile.updateAvatar({
|
||||
avatarHash: 'hash',
|
||||
avatarMime: 'image/png',
|
||||
avatarUrl: '/avatar.png'
|
||||
});
|
||||
|
||||
expect(context.store.dispatch).toHaveBeenCalledWith(UsersActions.updateCurrentUserProfile({
|
||||
profile: expect.objectContaining({ displayName: 'Plugin User' })
|
||||
}));
|
||||
|
||||
expect(context.store.dispatch).toHaveBeenCalledWith(UsersActions.updateCurrentUserAvatar({
|
||||
avatar: expect.objectContaining({ avatarUrl: '/avatar.png' })
|
||||
}));
|
||||
});
|
||||
|
||||
it('sends plugin messages and broadcasts them to peers', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
const message = api.messages.send('hello plugin');
|
||||
|
||||
expect(message.content).toBe('hello plugin');
|
||||
expect(message.roomId).toBe('room-1');
|
||||
expect(context.store.dispatch).toHaveBeenCalledWith(MessagesActions.sendMessageSuccess({ message }));
|
||||
expect(context.voice.broadcastMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'chat-message',
|
||||
message
|
||||
}));
|
||||
});
|
||||
|
||||
it('publishes typing state through the realtime facade', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
api.messages.setTyping(true, 'general');
|
||||
|
||||
expect(context.realtime.sendRawMessage).toHaveBeenCalledWith({
|
||||
type: 'typing',
|
||||
serverId: 'room-1',
|
||||
channelId: 'general',
|
||||
isTyping: true
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards slash command registration to the UI registry', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
const contribution = {
|
||||
name: 'echo',
|
||||
run: () => {}
|
||||
};
|
||||
|
||||
api.commands.register('echo', contribution);
|
||||
|
||||
expect(context.uiRegistry.registerSlashCommand).toHaveBeenCalledWith(
|
||||
TEST_MANIFEST.id,
|
||||
'echo',
|
||||
contribution
|
||||
);
|
||||
});
|
||||
|
||||
it('lists slash commands from the UI registry', () => {
|
||||
const commands = [{ name: 'echo', run: () => {} }];
|
||||
|
||||
context.uiRegistry.slashCommands.mockReturnValue(commands);
|
||||
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
expect(api.commands.list()).toBe(commands);
|
||||
});
|
||||
|
||||
it('routes storage APIs through the plugin storage service', async () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
api.storage.set('key', { ok: true });
|
||||
api.storage.get('key');
|
||||
api.storage.remove('key');
|
||||
await api.clientData.write('client-key', { ok: true });
|
||||
await api.clientData.read('client-key');
|
||||
await api.clientData.remove('client-key');
|
||||
await api.serverData.write('server-key', { ok: true });
|
||||
await api.serverData.read('server-key');
|
||||
await api.serverData.remove('server-key');
|
||||
|
||||
expect(context.storage.setLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key', { ok: true });
|
||||
expect(context.storage.getLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key');
|
||||
expect(context.storage.removeLocal).toHaveBeenCalledWith(TEST_MANIFEST.id, 'key');
|
||||
expect(context.storage.writeClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key', { ok: true });
|
||||
expect(context.storage.readClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key');
|
||||
expect(context.storage.removeClientData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'client-key');
|
||||
expect(context.storage.writeServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key', { ok: true });
|
||||
expect(context.storage.readServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key');
|
||||
expect(context.storage.removeServerData).toHaveBeenCalledWith(TEST_MANIFEST.id, 'server-key');
|
||||
});
|
||||
|
||||
it('publishes declared server events through the realtime facade', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
api.events.publishServer('e2e:server', { ok: true });
|
||||
|
||||
expect(context.realtime.sendRawMessage).toHaveBeenCalledWith(expect.objectContaining({
|
||||
type: 'plugin_event',
|
||||
eventName: 'e2e:server',
|
||||
payload: { ok: true },
|
||||
pluginId: TEST_MANIFEST.id,
|
||||
serverId: 'room-1'
|
||||
}));
|
||||
});
|
||||
|
||||
it('rejects undeclared plugin events', () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
expect(() => api.events.publishServer('missing:event', {})).toThrow(/did not declare event/);
|
||||
});
|
||||
|
||||
it('enforces capability grants for privileged API methods', async () => {
|
||||
const api = context.service.createApi(TEST_MANIFEST);
|
||||
|
||||
context.capabilities.revokeAll(TEST_MANIFEST.id);
|
||||
|
||||
for (const path of collectPluginApiMethodPaths()) {
|
||||
const capability = PLUGIN_CLIENT_API_CAPABILITY_REQUIREMENTS[path as PluginClientApiMethodPath];
|
||||
|
||||
if (!capability) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [namespace, method] = path.split('.') as [keyof typeof api, string];
|
||||
const target = api[namespace] as Record<string, (...args: unknown[]) => unknown>;
|
||||
|
||||
await expect(invokeApiMethod(target[method], path)).rejects.toThrow(PluginCapabilityError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface ServiceTestContext {
|
||||
capabilities: PluginCapabilityService;
|
||||
channels: ReturnType<typeof signal<Channel[]>>;
|
||||
currentUser: ReturnType<typeof signal<User | null>>;
|
||||
logger: {
|
||||
debug: ReturnType<typeof vi.fn>;
|
||||
error: ReturnType<typeof vi.fn>;
|
||||
info: ReturnType<typeof vi.fn>;
|
||||
warn: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
messages: ReturnType<typeof signal<Message[]>>;
|
||||
realtime: {
|
||||
onSignalingMessage: Subject<unknown>;
|
||||
sendRawMessage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
room: ReturnType<typeof signal<Room | null>>;
|
||||
service: PluginClientApiService;
|
||||
storage: {
|
||||
getLocal: ReturnType<typeof vi.fn>;
|
||||
readClientData: ReturnType<typeof vi.fn>;
|
||||
readServerData: ReturnType<typeof vi.fn>;
|
||||
removeClientData: ReturnType<typeof vi.fn>;
|
||||
removeLocal: ReturnType<typeof vi.fn>;
|
||||
removeServerData: ReturnType<typeof vi.fn>;
|
||||
setLocal: ReturnType<typeof vi.fn>;
|
||||
writeClientData: ReturnType<typeof vi.fn>;
|
||||
writeServerData: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
store: {
|
||||
dispatch: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
uiRegistry: {
|
||||
registerSlashCommand: ReturnType<typeof vi.fn>;
|
||||
slashCommands: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
voice: {
|
||||
broadcastMessage: ReturnType<typeof vi.fn>;
|
||||
getConnectedPeers: ReturnType<typeof vi.fn>;
|
||||
setInputVolume: ReturnType<typeof vi.fn>;
|
||||
setLocalStream: ReturnType<typeof vi.fn>;
|
||||
setOutputVolume: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
}
|
||||
|
||||
function createServiceTestContext(): ServiceTestContext {
|
||||
installLocalStorageMock();
|
||||
installBrowserMediaMocks();
|
||||
|
||||
const currentUser = signal<User | null>(createUser());
|
||||
const users = signal<User[]>(currentUser() ? [currentUser() as User] : []);
|
||||
const room = signal<Room | null>(createRoom());
|
||||
const channels = signal<Channel[]>(room()?.channels ?? []);
|
||||
const messages = signal<Message[]>([]);
|
||||
const activeChannelId = signal('general');
|
||||
const roomId = signal('room-1');
|
||||
const logger = {
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn()
|
||||
};
|
||||
const storage = {
|
||||
getLocal: vi.fn(),
|
||||
setLocal: vi.fn(),
|
||||
removeLocal: vi.fn(),
|
||||
readClientData: vi.fn(async () => null),
|
||||
writeClientData: vi.fn(async () => undefined),
|
||||
removeClientData: vi.fn(async () => undefined),
|
||||
readServerData: vi.fn(async () => null),
|
||||
writeServerData: vi.fn(async () => undefined),
|
||||
removeServerData: vi.fn(async () => undefined)
|
||||
};
|
||||
const uiRegistry = {
|
||||
registerSlashCommand: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
slashCommands: vi.fn(() => [])
|
||||
};
|
||||
const voice = {
|
||||
broadcastMessage: vi.fn(),
|
||||
getConnectedPeers: vi.fn(() => []),
|
||||
setInputVolume: vi.fn(),
|
||||
setLocalStream: vi.fn(async () => undefined),
|
||||
setOutputVolume: vi.fn()
|
||||
};
|
||||
const realtime = {
|
||||
onSignalingMessage: new Subject<unknown>(),
|
||||
sendRawMessage: vi.fn()
|
||||
};
|
||||
const store = {
|
||||
dispatch: vi.fn(),
|
||||
selectSignal: vi.fn((selector: unknown) => {
|
||||
if (selector === selectCurrentUser) {
|
||||
return currentUser;
|
||||
}
|
||||
|
||||
if (selector === selectAllUsers) {
|
||||
return users;
|
||||
}
|
||||
|
||||
if (selector === selectCurrentRoom) {
|
||||
return room;
|
||||
}
|
||||
|
||||
if (selector === selectCurrentRoomChannels) {
|
||||
return channels;
|
||||
}
|
||||
|
||||
if (selector === selectCurrentRoomMessages) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
if (selector === selectActiveChannelId) {
|
||||
return activeChannelId;
|
||||
}
|
||||
|
||||
if (selector === selectCurrentRoomId) {
|
||||
return roomId;
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected selector in PluginClientApiService test: ${String(selector)}`);
|
||||
})
|
||||
};
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
PluginClientApiService,
|
||||
PluginCapabilityService,
|
||||
{
|
||||
provide: AttachmentFacade,
|
||||
useValue: {
|
||||
publishAttachments: vi.fn(async () => undefined),
|
||||
rememberMessageRoom: vi.fn()
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
saveMessage: vi.fn(async () => undefined),
|
||||
updateMessage: vi.fn(async () => undefined),
|
||||
updateRoom: vi.fn(async () => undefined)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: PluginDesktopStateService,
|
||||
useValue: {
|
||||
readJson: vi.fn(async () => null),
|
||||
writeJson: vi.fn(async () => undefined)
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: PluginLoggerService,
|
||||
useValue: logger
|
||||
},
|
||||
{
|
||||
provide: PluginMessageBusService,
|
||||
useValue: {
|
||||
publish: vi.fn(() => ({ topic: 'test' })),
|
||||
sendLatestMessages: vi.fn(() => ({ topic: 'test' })),
|
||||
subscribe: vi.fn(() => ({ dispose: vi.fn() }))
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: PluginStorageService,
|
||||
useValue: storage
|
||||
},
|
||||
{
|
||||
provide: PluginUiRegistryService,
|
||||
useValue: uiRegistry
|
||||
},
|
||||
{
|
||||
provide: RealtimeSessionFacade,
|
||||
useValue: {
|
||||
broadcastMessage: vi.fn(),
|
||||
onSignalingMessage: realtime.onSignalingMessage.asObservable(),
|
||||
sendRawMessage: realtime.sendRawMessage
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: ServerDirectoryFacade,
|
||||
useValue: {
|
||||
updateServer: vi.fn(() => ({ subscribe: vi.fn() }))
|
||||
}
|
||||
},
|
||||
{
|
||||
provide: Store,
|
||||
useValue: store
|
||||
},
|
||||
{
|
||||
provide: VoiceConnectionFacade,
|
||||
useValue: voice
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
capabilities: injector.get(PluginCapabilityService),
|
||||
channels,
|
||||
currentUser,
|
||||
logger,
|
||||
messages,
|
||||
realtime,
|
||||
room,
|
||||
service: injector.get(PluginClientApiService),
|
||||
storage,
|
||||
store,
|
||||
uiRegistry,
|
||||
voice
|
||||
};
|
||||
}
|
||||
|
||||
function createTestManifest(): TojuPluginManifest {
|
||||
return {
|
||||
apiVersion: '1.0.0',
|
||||
capabilities: [
|
||||
'profile.read',
|
||||
'profile.write',
|
||||
'users.read',
|
||||
'users.manage',
|
||||
'roles.read',
|
||||
'roles.manage',
|
||||
'messages.read',
|
||||
'messages.send',
|
||||
'messages.editOwn',
|
||||
'messages.deleteOwn',
|
||||
'messages.moderate',
|
||||
'messages.sync',
|
||||
'channels.read',
|
||||
'channels.manage',
|
||||
'server.read',
|
||||
'server.manage',
|
||||
'p2p.data',
|
||||
'media.playAudio',
|
||||
'media.addAudioStream',
|
||||
'media.addVideoStream',
|
||||
'audio.volume',
|
||||
'ui.settings',
|
||||
'ui.pages',
|
||||
'ui.sidePanel',
|
||||
'ui.channelsSection',
|
||||
'ui.embeds',
|
||||
'ui.dom',
|
||||
'ui.commands',
|
||||
'storage.local',
|
||||
'storage.serverData.read',
|
||||
'storage.serverData.write',
|
||||
'events.server.publish',
|
||||
'events.server.subscribe',
|
||||
'events.p2p.publish',
|
||||
'events.p2p.subscribe'
|
||||
],
|
||||
compatibility: {
|
||||
minimumTojuVersion: '1.0.0'
|
||||
},
|
||||
description: 'Plugin API service test fixture',
|
||||
events: [
|
||||
{
|
||||
direction: 'serverRelay',
|
||||
eventName: 'e2e:server',
|
||||
scope: 'server'
|
||||
}
|
||||
],
|
||||
id: 'test.plugin-api',
|
||||
kind: 'client',
|
||||
schemaVersion: 1,
|
||||
title: 'Plugin API Service Fixture',
|
||||
version: '1.0.0'
|
||||
};
|
||||
}
|
||||
|
||||
function createUser(): User {
|
||||
return {
|
||||
displayName: 'Alice',
|
||||
id: 'user-1',
|
||||
isOnline: true,
|
||||
joinedAt: Date.now(),
|
||||
oderId: 'user-1',
|
||||
role: 'host',
|
||||
status: 'online',
|
||||
username: 'alice'
|
||||
};
|
||||
}
|
||||
|
||||
function createRoom(): Room {
|
||||
return {
|
||||
channels: [{ id: 'general', name: 'general', position: 0, type: 'text' }, { id: 'voice', name: 'voice', position: 1, type: 'voice' }],
|
||||
description: 'Plugin API room',
|
||||
hostId: 'user-1',
|
||||
id: 'room-1',
|
||||
isPrivate: false,
|
||||
members: [],
|
||||
name: 'Plugin API Room',
|
||||
roles: []
|
||||
};
|
||||
}
|
||||
|
||||
async function invokeApiMethod(method: (...args: unknown[]) => unknown, path: string): Promise<unknown> {
|
||||
switch (path) {
|
||||
case 'attachments.import':
|
||||
return method({ files: [], messageId: 'message-1' });
|
||||
case 'channels.addAudioChannel':
|
||||
return method({ name: 'Audio' });
|
||||
case 'channels.addTextChannel':
|
||||
return method({ name: 'Text' });
|
||||
case 'channels.addVideoChannel':
|
||||
return method({ name: 'Video' });
|
||||
case 'channels.remove':
|
||||
return method('general');
|
||||
case 'channels.rename':
|
||||
return method('general', 'renamed');
|
||||
case 'channels.select':
|
||||
return method('general');
|
||||
case 'clientData.read':
|
||||
case 'serverData.read':
|
||||
return method('key');
|
||||
case 'clientData.remove':
|
||||
case 'serverData.remove':
|
||||
return method('key');
|
||||
case 'clientData.write':
|
||||
case 'serverData.write':
|
||||
return method('key', { ok: true });
|
||||
case 'commands.register':
|
||||
return method('echo', { name: 'echo', run: () => {} });
|
||||
case 'events.publishP2p':
|
||||
case 'events.publishServer':
|
||||
return method('e2e:server', {});
|
||||
case 'events.subscribeP2p':
|
||||
case 'events.subscribeServer':
|
||||
return method({ eventName: 'e2e:server', handler: () => {} });
|
||||
case 'media.addCustomAudioStream':
|
||||
case 'media.addCustomVideoStream':
|
||||
return method({ stream: new MediaStream() });
|
||||
case 'media.playAudioClip':
|
||||
return method({ url: 'data:audio/wav;base64,' });
|
||||
case 'media.setInputVolume':
|
||||
case 'media.setOutputVolume':
|
||||
return method(0.5);
|
||||
case 'messageBus.publish':
|
||||
return method({ topic: 'test' });
|
||||
case 'messageBus.sendLatestMessages':
|
||||
return method({});
|
||||
case 'messageBus.subscribe':
|
||||
return method({ handler: () => {} });
|
||||
case 'messages.delete':
|
||||
return method('message-1');
|
||||
case 'messages.edit':
|
||||
return method('message-1', 'updated');
|
||||
case 'messages.moderateDelete':
|
||||
return method('message-1');
|
||||
case 'messages.import':
|
||||
return method([]);
|
||||
case 'messages.send':
|
||||
return method('hello');
|
||||
case 'messages.sendAsPluginUser':
|
||||
return method({ content: 'hello', pluginUserId: 'bot' });
|
||||
case 'messages.setTyping':
|
||||
return method(true);
|
||||
case 'messages.subscribeTyping':
|
||||
return method(() => {});
|
||||
case 'messages.sync':
|
||||
return method([]);
|
||||
case 'p2p.broadcastData':
|
||||
return method('e2e:server', {});
|
||||
case 'p2p.sendData':
|
||||
return method('peer-1', 'e2e:server', {});
|
||||
case 'profile.update':
|
||||
return method({ displayName: 'Updated' });
|
||||
case 'profile.updateAvatar':
|
||||
return method({ avatarHash: 'hash', avatarMime: 'image/png', avatarUrl: '/avatar.png' });
|
||||
case 'roles.setAssignments':
|
||||
return method([]);
|
||||
case 'server.registerPluginUser':
|
||||
return method({ displayName: 'Bot' });
|
||||
case 'server.updateIcon':
|
||||
return method('icon-hash');
|
||||
case 'server.updatePermissions':
|
||||
return method({ allowVoice: true });
|
||||
case 'server.updateSettings':
|
||||
return method({ name: 'Room' });
|
||||
case 'storage.get':
|
||||
return method('key');
|
||||
case 'storage.remove':
|
||||
return method('key');
|
||||
case 'storage.set':
|
||||
return method('key', { ok: true });
|
||||
case 'ui.mountElement':
|
||||
return method('mount', { element: { tagName: 'DIV' }, target: 'body' });
|
||||
case 'ui.registerAppPage':
|
||||
case 'ui.registerChannelSection':
|
||||
case 'ui.registerComposerAction':
|
||||
case 'ui.registerEmbedRenderer':
|
||||
case 'ui.registerProfileAction':
|
||||
case 'ui.registerSettingsPage':
|
||||
case 'ui.registerSidePanel':
|
||||
case 'ui.registerToolbarAction':
|
||||
return method('id', { label: 'Test', render: () => 'ok', run: () => {} });
|
||||
case 'users.ban':
|
||||
return method('user-2', 'reason');
|
||||
case 'users.kick':
|
||||
return method('user-2');
|
||||
case 'users.setRole':
|
||||
return method('user-2', 'member');
|
||||
default:
|
||||
return Promise.resolve(method());
|
||||
}
|
||||
}
|
||||
|
||||
function installBrowserMediaMocks(): void {
|
||||
vi.stubGlobal('MediaStream', class MediaStream {});
|
||||
vi.stubGlobal('Audio', class Audio {
|
||||
volume = 1;
|
||||
|
||||
async play(): Promise<void> {}
|
||||
});
|
||||
}
|
||||
|
||||
function installLocalStorageMock(): void {
|
||||
const storage = new Map<string, string>();
|
||||
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => storage.get(key) ?? null,
|
||||
setItem: (key: string, value: string) => {
|
||||
storage.set(key, value);
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
storage.delete(key);
|
||||
},
|
||||
clear: () => {
|
||||
storage.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user