snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-16 01:21:02 +05:00
commit 30810eecd7
10161 changed files with 987110 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
import client from '@rocket.chat/jest-presets/client';
import type { Config } from 'jest';
export default {
preset: client.preset,
modulePathIgnorePatterns: ['<rootDir>/__tests__/helpers'],
testMatch: ['<rootDir>/src/tests/**/**.spec.[jt]s?(x)'],
} satisfies Config;

View File

@@ -0,0 +1,53 @@
{
"name": "@rocket.chat/mock-providers",
"version": "0.4.20",
"private": true,
"main": "./dist/index.js",
"typings": "./dist/index.d.ts",
"files": [
"/dist"
],
"scripts": {
"build": "rm -rf dist && tsc -p tsconfig.json",
"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"test": "jest",
"testunit": "jest"
},
"dependencies": {
"@rocket.chat/emitter": "workspace:~",
"@rocket.chat/i18n": "workspace:~",
"@rocket.chat/ui-contexts": "workspace:^",
"i18next": "~23.4.9",
"react-i18next": "~13.2.2"
},
"devDependencies": {
"@faker-js/faker": "~8.0.2",
"@rocket.chat/ddp-client": "workspace:~",
"@rocket.chat/mongo-adapter": "workspace:~",
"@rocket.chat/tools": "workspace:~",
"@rocket.chat/ui-video-conf": "workspace:*",
"@storybook/react": "^9.1.20",
"@tanstack/react-query": "~5.65.1",
"@testing-library/dom": "~10.4.1",
"@testing-library/jest-dom": "~6.8.0",
"@testing-library/react": "~16.3.3",
"@types/react": "~19.2.18",
"@types/react-dom": "~19.2.5",
"eslint": "~9.39.5",
"jest": "~30.2.0",
"react": "~19.2.8",
"react-dom": "~19.2.8",
"storybook": "^9.1.20",
"typescript": "~5.9.3"
},
"peerDependencies": {
"@storybook/react": "*",
"@tanstack/react-query": "*",
"react": "*"
},
"volta": {
"extends": "../../package.json"
}
}

View File

@@ -0,0 +1,905 @@
import type {
CallPreferences,
DirectCallData,
IRole,
IRoom,
ISetting,
IUser,
ProviderCapabilities,
Serialized,
SettingValue,
} from '@rocket.chat/core-typings';
import type {
ServerMethodName,
ServerMethodParameters,
ServerMethodReturn,
StreamerCallback,
StreamerCallbackArgs,
StreamerEvents,
StreamKeys,
StreamNames,
} from '@rocket.chat/ddp-client';
import { Emitter } from '@rocket.chat/emitter';
import languages from '@rocket.chat/i18n/dist/languages';
import { createPredicateFromFilter } from '@rocket.chat/mongo-adapter';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import type {
Device,
DeviceContext,
LoginService,
ModalContextValue,
ServerContextValue,
SettingsContextQuery,
SubscriptionWithRoom,
ToastMessagesContextValue,
TranslationKey,
} from '@rocket.chat/ui-contexts';
import {
AuthorizationContext,
RouterContext,
ServerContext,
SettingsContext,
TranslationContext,
UserContext,
ActionManagerContext,
ModalContext,
UserPresenceContext,
AuthenticationContext,
ToastMessagesContext,
} from '@rocket.chat/ui-contexts';
import type { VideoConfPopupPayload } from '@rocket.chat/ui-video-conf';
import { VideoConfContext } from '@rocket.chat/ui-video-conf';
import type { Decorator } from '@storybook/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createInstance } from 'i18next';
import type { ObjectId } from 'mongodb';
import type { ContextType, JSXElementConstructor, ReactNode } from 'react';
import { useEffect, useReducer, useSyncExternalStore } from 'react';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { MockedDeviceContext } from './MockedDeviceContext';
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// interface MockedAppRootEvents extends Record<`stream-${StreamNames}-${StreamKeys<StreamNames>}`, any> {
// 'update-modal': void;
// }
// Extract all key values from objects that have a 'key' property
type ExtractKeys<T, N extends string> = T extends readonly (infer U)[]
? U extends { key: infer K }
? K extends string
? string extends K
? never
: `stream-${N}-${K}`
: never
: never
: never;
// Union of all key values from all streams
type AllStreamerEventKeys = {
[K in keyof StreamerEvents]: ExtractKeys<StreamerEvents[K], K>;
}[keyof StreamerEvents];
type MockedAppRootEvents = {
'update-modal': void;
} & Record<AllStreamerEventKeys, any>;
export type StreamControllerRef<N extends StreamNames> = {
controller?: {
emit: <K extends StreamKeys<N>>(eventName: K, args: StreamerCallbackArgs<N, K>) => void;
has: (eventName: StreamKeys<N>) => boolean;
};
};
const empty = [] as const;
export class MockedAppRootBuilder {
private _settings: Map<string, ISetting> = new Map();
private wrappers: Array<(children: ReactNode) => ReactNode> = [];
private server: ContextType<typeof ServerContext> = {
connected: true,
status: 'connected',
retryCount: 0,
info: undefined,
absoluteUrl: (path: string) => `http://localhost:3000/${path}`,
callEndpoint: <TMethod extends Method, TPathPattern extends PathPattern>({
method,
pathPattern,
}: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
throw new Error(`not implemented (method: ${method}, pathPattern: ${pathPattern})`);
},
getStream: () => () => () => undefined,
getStreamAll: () => () => () => undefined,
uploadToEndpoint: () => Promise.reject(new Error('not implemented')),
callMethod: () => Promise.reject(new Error('not implemented')),
disconnect: () => {
throw new Error('not implemented');
},
reconnect: () => {
throw new Error('not implemented');
},
writeStream: () => {
throw new Error('not implemented');
},
};
private router: ContextType<typeof RouterContext> = {
buildRoutePath: () => '/',
defineRoutes: () => () => undefined,
getLocationPathname: () => '/',
getLocationSearch: () => '',
getLocationHash: () => '',
getRouteName: () => undefined,
getPreviousRouteName: () => undefined,
getRouteParameters: () => ({}),
getSearchParameters: () => ({}),
navigate: () => undefined,
subscribeToRouteChange: () => () => undefined,
getRoomRoute: () => ({ path: '/' }),
};
private settings: Mutable<ContextType<typeof SettingsContext>> = {
hasPrivateAccess: true,
querySetting: (_id: string) => [() => () => undefined, () => undefined],
querySettings: (_query: SettingsContextQuery) => [() => () => undefined, () => empty as unknown as ISetting[]],
dispatch: async () => undefined,
};
private user: ContextType<typeof UserContext> = {
logout: () => Promise.reject(new Error('not implemented')),
onLogout: () => () => undefined,
queryPreference: () => [() => () => undefined, () => undefined],
queryRoom: () => [() => () => undefined, () => this.room],
querySubscription: () => [() => () => undefined, () => this.subscription],
querySubscriptions: () => [
() => () => undefined,
() => (this.subscription ? [this.subscription, ...(this.subscriptions ?? [])] : (this.subscriptions ?? [])),
], // apply query and option
user: null,
userId: undefined,
};
private toastMessages: ToastMessagesContextValue = {
dispatch: () => undefined,
};
private userPresence: ContextType<typeof UserPresenceContext> = {
queryUserData: (_uid) => ({ subscribe: () => () => undefined, get: () => undefined }),
};
private videoConf: ContextType<typeof VideoConfContext> = {
queryIncomingCalls: () => [() => () => undefined, () => []],
queryRinging: () => [() => () => undefined, () => false],
queryCalling: () => [() => () => undefined, () => false],
dispatchOutgoing(_options: Omit<VideoConfPopupPayload, 'id'>): void {
throw new Error('Function not implemented.');
},
dismissOutgoing(): void {
throw new Error('Function not implemented.');
},
startCall(_rid: IRoom['_id'], _title?: string): void {
throw new Error('Function not implemented.');
},
acceptCall(_callId: string): void {
throw new Error('Function not implemented.');
},
joinCall(_callId: string): void {
throw new Error('Function not implemented.');
},
dismissCall(_callId: string): void {
throw new Error('Function not implemented.');
},
rejectIncomingCall(_callId: string): void {
throw new Error('Function not implemented.');
},
abortCall(): void {
throw new Error('Function not implemented.');
},
setPreferences(_prefs: { mic?: boolean; cam?: boolean }): void {
throw new Error('Function not implemented.');
},
loadCapabilities(): Promise<void> {
throw new Error('Function not implemented.');
},
queryCapabilities(): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ProviderCapabilities] {
throw new Error('Function not implemented.');
},
queryPreferences(): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => CallPreferences] {
throw new Error('Function not implemented.');
},
};
private room: IRoom | undefined = undefined;
private subscriptions: SubscriptionWithRoom[] | undefined = undefined;
private subscription: SubscriptionWithRoom | undefined = undefined;
private modal: ModalContextValue = {
currentModal: { component: null },
modal: {
setModal: (modal) => {
this.modal = {
...this.modal,
currentModal: { component: modal },
};
this.events.emit('update-modal');
},
},
};
// Mutated by `withRoleDefinition` before render, then held stable, so the identity is
// a safe `useSyncExternalStore` snapshot.
private rolesMap = new Map<IRole['_id'], IRole>();
private authorization: ContextType<typeof AuthorizationContext> = {
queryPermission: () => [() => () => undefined, () => false],
queryAtLeastOnePermission: () => [() => () => undefined, () => false],
queryAllPermissions: () => [() => () => undefined, () => false],
queryRole: () => [() => () => undefined, () => false],
getRoles: () => this.rolesMap,
subscribeToRoles: () => () => undefined,
};
private authServices: LoginService[] = [];
private authentication: ContextType<typeof AuthenticationContext> = {
isLoggingIn: false,
loginWithPassword: () => Promise.resolve(),
loginWithToken: () => Promise.resolve(),
loginWithService: () => () => Promise.resolve(true),
loginWithCustomOauth: () => undefined,
loginWithIframe: async () => Promise.reject('loginWithIframe not implemented'),
loginWithTokenRoute: async () => Promise.reject('loginWithTokenRoute not implemented'),
queryLoginServices: {
getCurrentValue: () => this.authServices,
subscribe: () => () => undefined,
},
getLoginToken: () => null,
unstoreLoginToken: () => () => undefined,
wipeLocalAuth: () => undefined,
};
private events = new Emitter<MockedAppRootEvents>();
private deviceContext: Partial<ContextType<typeof DeviceContext>> = {
enabled: true,
availableAudioOutputDevices: [],
availableAudioInputDevices: [],
selectedAudioOutputDevice: undefined,
selectedAudioInputDevice: undefined,
setAudioOutputDevice: () => undefined,
setAudioInputDevice: () => undefined,
permissionStatus: undefined,
};
private _providedQueryClient: QueryClient | undefined;
private get queryClient(): QueryClient {
return (
this._providedQueryClient ||
new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
})
);
}
wrap(wrapper: (children: ReactNode) => ReactNode): this {
this.wrappers.push(wrapper);
return this;
}
withEndpoint<TMethod extends Method, TPathPattern extends PathPattern>(
method: TMethod,
pathPattern: TPathPattern,
response: (
params: OperationParams<TMethod, TPathPattern>,
) => Serialized<OperationResult<TMethod, TPathPattern>> | Promise<Serialized<OperationResult<TMethod, TPathPattern>>>,
): this {
const innerFn = this.server.callEndpoint;
const outerFn = <TMethod extends Method, TPathPattern extends PathPattern>(args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}): Promise<Serialized<OperationResult<TMethod, TPathPattern>>> => {
if (args.method === String(method) && args.pathPattern === String(pathPattern)) {
return Promise.resolve(response(args.params)) as Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
}
return innerFn(args);
};
this.server.callEndpoint = outerFn;
return this;
}
withStream<N extends StreamNames>(streamName: N, ref: StreamControllerRef<N>): this {
const innerFn = this.server.getStream;
const outerFn: ServerContextValue['getStream'] = (innerStreamName) => {
if (innerStreamName === (streamName as StreamNames)) {
ref.controller = {
emit: <K extends StreamKeys<N>>(eventName: K, args: StreamerCallbackArgs<N, K>) => {
this.events.emit(`stream-${innerStreamName}-${eventName}` as AllStreamerEventKeys, ...args);
},
has: (eventName: string) => this.events.has(`stream-${innerStreamName}-${eventName}` as AllStreamerEventKeys),
};
return <K extends StreamKeys<N>>(eventName: K, callback: StreamerCallback<N, K>) =>
this.events.on(`stream-${innerStreamName}-${eventName}` as AllStreamerEventKeys, callback);
}
return innerFn(innerStreamName);
};
this.server.getStream = outerFn;
return this;
}
withMethod<TMethodName extends ServerMethodName>(methodName: TMethodName, response: () => ServerMethodReturn<TMethodName>): this {
const innerFn = this.server.callMethod;
const outerFn = <TMethodName extends ServerMethodName>(
innerMethodName: TMethodName,
...innerArgs: ServerMethodParameters<TMethodName>
): Promise<ServerMethodReturn<TMethodName>> => {
if (innerMethodName === String(methodName)) {
return Promise.resolve(response()) as Promise<ServerMethodReturn<TMethodName>>;
}
if (!innerFn) {
throw new Error('not implemented');
}
return innerFn(innerMethodName, ...innerArgs);
};
this.server.callMethod = outerFn;
return this;
}
withPermission(permission: string): this {
const innerFn = this.authorization.queryPermission;
const outerFn = (
innerPermission: string | ObjectId,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermission === permission) {
return [() => () => undefined, () => true];
}
return innerFn(innerPermission, innerScope, innerScopedRoles);
};
this.authorization.queryPermission = outerFn;
const innerFn2 = this.authorization.queryAtLeastOnePermission;
const outerFn2 = (
innerPermissions: Array<string | ObjectId>,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermissions.includes(permission)) {
return [() => () => undefined, () => true];
}
return innerFn2(innerPermissions, innerScope, innerScopedRoles);
};
this.authorization.queryAtLeastOnePermission = outerFn2;
const innerFn3 = this.authorization.queryAllPermissions;
const outerFn3 = (
innerPermissions: Array<string | ObjectId>,
innerScope?: string | ObjectId | undefined,
innerScopedRoles?: string[] | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerPermissions.includes(permission)) {
return [() => () => undefined, () => true];
}
return innerFn3(innerPermissions, innerScope, innerScopedRoles);
};
this.authorization.queryAllPermissions = outerFn3;
return this;
}
withJohnDoe(overrides: Partial<IUser> = {}): this {
this.user.userId = 'john.doe';
this.user.user = {
_id: 'john.doe',
username: 'john.doe',
name: 'John Doe',
createdAt: new Date(),
active: true,
_updatedAt: new Date(),
roles: ['admin'],
type: 'user',
...overrides,
};
return this;
}
withAnonymous(): this {
this.user.userId = undefined;
this.user.user = null;
return this;
}
withUser(user: IUser): this {
this.user.userId = user._id;
this.user.user = user;
return this;
}
withLogout(logout: ContextType<typeof UserContext>['logout']): this {
this.user = { ...this.user, logout };
return this;
}
withToastMessageDispatch(dispatch: ToastMessagesContextValue['dispatch']): this {
this.toastMessages = { ...this.toastMessages, dispatch };
return this;
}
withUsers(users: IUser[]): this {
users.forEach((user) => {
this.userPresence.queryUserData = (_uid) => ({ subscribe: () => () => undefined, get: () => user });
});
return this;
}
withSubscriptions(subscriptions: SubscriptionWithRoom[]): this {
this.subscriptions = subscriptions;
return this;
}
withSubscription(subscription: SubscriptionWithRoom): this {
this.subscription = subscription;
return this;
}
withRoom(room: IRoom): this {
this.room = room;
return this;
}
withRouter(overrides: Partial<ContextType<typeof RouterContext>>): this {
this.router = { ...this.router, ...overrides };
return this;
}
withRouteParameter(name: string, value: string): this {
const innerFn = this.router.getRouteParameters;
this.router.getRouteParameters = () => ({ ...innerFn(), [name]: value });
return this;
}
withRole(role: string): this {
if (!this.user.user) {
throw new Error('user is not defined');
}
this.user.user.roles.push(role);
const innerFn = this.authorization.queryRole;
const outerFn = (
innerRole: string | ObjectId,
innerScope?: string | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerRole === role) {
return [() => () => undefined, () => true];
}
return innerFn(innerRole, innerScope);
};
this.authorization.queryRole = outerFn;
return this;
}
/**
* Grants a role scoped to `Subscriptions` — `owner`, `moderator`, `leader`, or a custom
* one — in a single room. Unlike {@link withRole}, the grant is not workspace-wide: a
* check only passes when it carries that room as its scope, which is how the real
* provider resolves a subscription role. The role is deliberately kept out of the user's
* `roles`, where only a `Users`-scoped grant belongs.
*/
withRoleScoped(role: string, scope: IRoom['_id']): this {
const innerFn = this.authorization.queryRole;
const outerFn = (
innerRole: string | ObjectId,
innerScope?: string | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => boolean] => {
if (innerRole === role && innerScope === scope) {
return [() => () => undefined, () => true];
}
return innerFn(innerRole, innerScope);
};
this.authorization.queryRole = outerFn;
return this;
}
/**
* Registers a role in the workspace roles map without granting it. A custom role
* has an id that differs from its name, so pass both to exercise code that
* resolves a name to an id. Chain `withRole(_id)` to grant it to the user.
*/
withRoleDefinition(role: Pick<IRole, '_id' | 'name'> & Partial<IRole>): this {
this.rolesMap.set(role._id, {
description: '',
protected: false,
scope: 'Users',
...role,
} as IRole);
return this;
}
withSetting(id: string, value: SettingValue, settingStructure?: Partial<ISetting>): this {
const setting = {
...settingStructure,
_id: id,
value,
} as ISetting;
const innerFn = this.settings.querySetting;
const outerFn = (
innerSetting: string,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => ISetting | undefined] => {
if (innerSetting === id) {
return [() => () => undefined, () => setting];
}
return innerFn(innerSetting);
};
this.settings.querySetting = outerFn;
this._settings.set(id, setting);
const cache = new WeakMap();
this.settings.querySettings = (query: SettingsContextQuery) => {
const filter =
cache.get(query) ??
createPredicateFromFilter({
...(query._id ? { _id: { $in: query._id } } : {}),
} as any);
cache.set(query, filter);
const arr = cache.get(filter) ?? Array.from(this._settings.values()).filter(filter);
return [
() => () => undefined,
() => {
return arr;
},
];
};
return this;
}
withUserPreference(id: string | ObjectId, value: unknown): this {
const innerFn = this.user.queryPreference;
const outerFn = <T,>(
key: string | ObjectId,
defaultValue?: T | undefined,
): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => T | undefined] => {
if (key === id) {
return [() => () => undefined, () => value as T];
}
return innerFn(key, defaultValue);
};
this.user.queryPreference = outerFn;
return this;
}
withIncomingCalls(calls: DirectCallData[]): this {
if (!this.videoConf) {
throw Error('videoConf is not defined');
}
const innerFn = this.videoConf.queryIncomingCalls;
const outerFn = (): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => DirectCallData[]] => {
if (calls.length) {
return [() => () => undefined, () => calls];
}
return innerFn();
};
this.videoConf.queryIncomingCalls = outerFn;
return this;
}
withOpenModal(modal: ReactNode) {
this.modal.currentModal = { component: modal };
return this;
}
withAudioInputDevices(devices: Device[]): this {
if (!this.deviceContext.enabled) {
throw new Error('DeviceContext is not enabled');
}
this.deviceContext.availableAudioInputDevices = devices;
return this;
}
withAudioOutputDevices(devices: Device[]): this {
if (!this.deviceContext.enabled) {
throw new Error('DeviceContext is not enabled');
}
this.deviceContext.availableAudioOutputDevices = devices;
return this;
}
withMicrophonePermissionState(status: PermissionStatus): this {
if (!this.deviceContext.enabled) {
throw new Error('DeviceContext is not enabled');
}
this.deviceContext.permissionStatus = status;
return this;
}
private i18n = createInstance({
// debug: true,
lng: 'en',
fallbackLng: 'en',
ns: ['core'],
nsSeparator: '.',
partialBundledLanguages: true,
defaultNS: 'core',
interpolation: {
escapeValue: false,
},
initImmediate: false,
}).use(initReactI18next);
withTranslations(lng: string, ns: string, resources: Record<string, string>): this {
const addResources = () => {
this.i18n.addResources(lng, ns, resources);
for (const [key, value] of Object.entries(resources)) {
this.i18n.addResource(lng, ns, key, value);
}
};
if (this.i18n.isInitialized) {
addResources();
return this;
}
this.i18n.on('initialized', addResources);
return this;
}
// Manually changes the language in the i18next instance
// To be used with languages other than the default one
withDefaultLanguage(lng: string): this {
if (this.i18n.isInitialized) {
void this.i18n.changeLanguage(lng);
return this;
}
this.i18n.on('initialized', () => {
void this.i18n.changeLanguage(lng);
});
return this;
}
withServerContext(partial: Partial<ServerContextValue>): this {
this.server = { ...this.server, ...partial };
return this;
}
withQueryClient(client: QueryClient): this {
this._providedQueryClient = client;
return this;
}
build(): JSXElementConstructor<{ children: ReactNode }> {
const {
queryClient,
server,
router,
settings,
user,
userPresence,
videoConf,
i18n,
authorization,
wrappers,
deviceContext,
authentication,
toastMessages,
} = this;
const reduceTranslation = (translation?: ContextType<typeof TranslationContext>): ContextType<typeof TranslationContext> => {
return {
...translation,
language: i18n.isInitialized ? i18n.language : 'en',
languages: [
{
en: 'Default',
name: i18n.isInitialized ? i18n.t('Default') : 'Default',
ogName: i18n.isInitialized ? i18n.t('Default') : 'Default',
key: '',
},
...(i18n.isInitialized
? [...new Set([...i18n.languages, ...languages])].map((key) => ({
en: key,
name: new Intl.DisplayNames([key], { type: 'language' }).of(key) ?? key,
ogName: new Intl.DisplayNames([key], { type: 'language' }).of(key) ?? key,
key,
}))
: []),
],
loadLanguage: async (language) => {
if (!i18n.isInitialized) {
return;
}
await i18n.changeLanguage(language);
},
translate: Object.assign(
(key: TranslationKey, options?: unknown) => (i18n.isInitialized ? i18n.t(key, options as { lng?: string }) : ''),
{
has: (key: string, options?: { lng?: string }): key is TranslationKey =>
!!key && i18n.isInitialized && i18n.exists(key, options),
},
),
};
};
const subscribeToModal = (onStoreChange: () => void) => this.events.on('update-modal', onStoreChange);
const getModalSnapshot = () => this.modal;
void i18n.init();
return function MockedAppRoot({ children }) {
const [translation, updateTranslation] = useReducer(reduceTranslation, undefined, () => reduceTranslation());
useEffect(() => {
i18n.on('initialized', updateTranslation);
i18n.on('languageChanged', updateTranslation);
return () => {
i18n.off('initialized', updateTranslation);
i18n.off('languageChanged', updateTranslation);
};
}, []);
const modal = useSyncExternalStore(subscribeToModal, getModalSnapshot);
return (
<QueryClientProvider client={queryClient}>
<ServerContext.Provider value={server}>
<RouterContext.Provider value={router}>
<SettingsContext.Provider value={settings}>
<I18nextProvider i18n={i18n}>
<TranslationContext.Provider value={translation}>
{/* <SessionProvider>
<TooltipProvider> */}
<ToastMessagesContext.Provider value={toastMessages}>
{/* <LayoutProvider>
<AvatarUrlProvider>
<CustomSoundProvider> */}
<UserContext.Provider value={user}>
<AuthenticationContext.Provider value={authentication}>
<MockedDeviceContext {...deviceContext}>
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<OmnichannelRoomIconProvider>
*/}
<UserPresenceContext.Provider value={userPresence}>
<ActionManagerContext.Provider
value={{
generateTriggerId: () => '',
emitInteraction: () => Promise.reject(new Error('not implemented')),
getInteractionPayloadByViewId: () => undefined,
handleServerInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
openView: () => undefined,
disposeView: () => undefined,
notifyBusy: () => undefined,
notifyIdle: () => undefined,
}}
>
<VideoConfContext.Provider value={videoConf}>
{/* <CallProvider>
<OmnichannelProvider> */}
{wrappers.reduce<ReactNode>(
(children, wrapper) => wrapper(children),
<>
{children}
{modal.currentModal.component}
</>,
)}
{/* </OmnichannelProvider>
</CallProvider> */}
</VideoConfContext.Provider>
</ActionManagerContext.Provider>
</UserPresenceContext.Provider>
{/*
</OmnichannelRoomIconProvider>
</EmojiPickerProvider>*/}
</AuthorizationContext.Provider>
</ModalContext.Provider>
</MockedDeviceContext>
</AuthenticationContext.Provider>
</UserContext.Provider>
{/* </CustomSoundProvider>
</AvatarUrlProvider>
</LayoutProvider> */}
</ToastMessagesContext.Provider>
{/* </TooltipProvider>
</SessionProvider> */}
</TranslationContext.Provider>
</I18nextProvider>
</SettingsContext.Provider>
</RouterContext.Provider>
</ServerContext.Provider>
</QueryClientProvider>
);
};
}
buildStoryDecorator(): Decorator {
const WrapperComponent = this.build();
// eslint-disable-next-line react/display-name, react/no-multi-comp
return (fn) => <WrapperComponent>{fn()}</WrapperComponent>;
}
}

View File

@@ -0,0 +1,32 @@
import { AuthorizationContext } from '@rocket.chat/ui-contexts';
import type { ContextType, ReactNode } from 'react';
const dummyRolesMap: ReturnType<ContextType<typeof AuthorizationContext>['getRoles']> = new Map();
export const MockedAuthorizationContext = ({
permissions = [],
roles = [],
children,
}: {
permissions: string[];
roles?: string[];
children: ReactNode;
}) => {
return (
<AuthorizationContext.Provider
value={{
queryPermission: (id: string) => [() => (): void => undefined, (): boolean => permissions.includes(id)],
queryAtLeastOnePermission: (ids: string[]) => [
() => (): void => undefined,
(): boolean => ids.some((id) => permissions.includes(id)),
],
queryAllPermissions: (ids: string[]) => [() => (): void => undefined, (): boolean => ids.every((id) => permissions.includes(id))],
queryRole: (id: string) => [() => (): void => undefined, (): boolean => roles.includes(id)],
getRoles: () => dummyRolesMap,
subscribeToRoles: (): (() => void) => (): void => undefined,
}}
>
{children}
</AuthorizationContext.Provider>
);
};

View File

@@ -0,0 +1,31 @@
import type { DeviceContextValue } from '@rocket.chat/ui-contexts';
import { DeviceContext } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
const mockPermissionStatus: PermissionStatus = {
state: 'granted',
name: 'microphone',
onchange: () => undefined,
addEventListener: () => undefined,
removeEventListener: () => undefined,
dispatchEvent: () => true,
};
const mockDeviceContextValue: DeviceContextValue = {
enabled: true,
selectedAudioOutputDevice: undefined,
selectedAudioInputDevice: undefined,
availableAudioOutputDevices: [],
availableAudioInputDevices: [],
permissionStatus: mockPermissionStatus,
setAudioOutputDevice: () => undefined,
setAudioInputDevice: () => undefined,
};
export type MockedDeviceContextProps = Partial<DeviceContextValue> & {
children: ReactNode;
};
export const MockedDeviceContext = ({ children, ...props }: MockedDeviceContextProps) => {
return <DeviceContext.Provider value={{ ...mockDeviceContextValue, ...props }}>{children}</DeviceContext.Provider>;
};

View File

@@ -0,0 +1,22 @@
import { ModalContext } from '@rocket.chat/ui-contexts';
import type { ReactNode } from 'react';
import { useState } from 'react';
export type MockedModalContextProps = { children: ReactNode };
export const MockedModalContext = ({ children }: MockedModalContextProps) => {
const [currentModal, setCurrentModal] = useState<ReactNode>(null);
return (
<ModalContext.Provider
value={{
modal: {
setModal: setCurrentModal,
},
currentModal: { component: currentModal },
}}
>
{children}
</ModalContext.Provider>
);
};

View File

@@ -0,0 +1,28 @@
import { RouterContext } from '@rocket.chat/ui-contexts';
import type { ContextType, ReactNode } from 'react';
export type MockedRouterContextProps = { children: ReactNode; router?: Partial<ContextType<typeof RouterContext>> };
export const MockedRouterContext = ({ children, router }: MockedRouterContextProps) => {
return (
<RouterContext.Provider
value={{
subscribeToRouteChange: () => () => undefined,
getLocationPathname: () => '/',
getLocationSearch: () => '',
getLocationHash: () => '',
getRouteParameters: () => ({}),
getSearchParameters: () => ({}),
getRouteName: () => undefined,
getPreviousRouteName: () => undefined,
buildRoutePath: () => '/',
navigate: () => undefined,
defineRoutes: () => () => undefined,
getRoomRoute: () => ({ path: '/' }),
...router,
}}
>
{children}
</RouterContext.Provider>
);
};

View File

@@ -0,0 +1,52 @@
import type { Serialized } from '@rocket.chat/core-typings';
import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn } from '@rocket.chat/ddp-client';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import { ServerContext } from '@rocket.chat/ui-contexts';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';
import { useState } from 'react';
export const MockedServerContext = ({
handleRequest,
handleMethod,
children,
}: {
handleRequest?: <TMethod extends Method, TPathPattern extends PathPattern>(args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}) => Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
handleMethod?: <MethodName extends ServerMethodName>(
methodName: MethodName,
...args: ServerMethodParameters<MethodName>
) => Promise<ServerMethodReturn<MethodName>>;
children: ReactNode;
isEnterprise?: boolean;
}): any => {
const [queryClient] = useState(() => new QueryClient());
return (
<ServerContext.Provider
value={
{
absoluteUrl: (path: string) => `http://localhost:3000/${path}`,
callMethod: <MethodName extends ServerMethodName>(methodName: MethodName, ...args: ServerMethodParameters<MethodName>) => {
return handleMethod?.(methodName, ...args);
},
callEndpoint: async <TMethod extends Method, TPathPattern extends PathPattern>(args: {
method: TMethod;
pathPattern: TPathPattern;
keys: UrlParams<TPathPattern>;
params: OperationParams<TMethod, TPathPattern>;
}) => {
return handleRequest?.(args);
},
getStream: () => () => undefined,
} as any
}
>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</ServerContext.Provider>
);
};

View File

@@ -0,0 +1,36 @@
import type { ISetting } from '@rocket.chat/core-typings';
import { SettingsContext } from '@rocket.chat/ui-contexts';
import type { ContextType, ReactNode } from 'react';
const settingContextValue: ContextType<typeof SettingsContext> = {
hasPrivateAccess: true,
querySetting: (_id: string) => [() => () => undefined, () => undefined],
querySettings: () => [() => () => undefined, () => []],
dispatch: async () => undefined,
};
const createSettingContextValue = ({ settings }: { settings?: Record<string, ISetting['value']> }): ContextType<typeof SettingsContext> => {
const cache = new Map<string, ISetting['value']>();
return {
...settingContextValue,
...(settings && {
querySetting: (_id: string) => [
() => () => undefined,
() => {
if (cache.has(_id)) {
return cache.get(_id) as any;
}
cache.set(_id, { value: settings[_id] } as any);
return cache.get(_id) as any;
},
],
}),
};
};
export type MockedSettingsContextProps = { children: ReactNode; settings?: Record<string, ISetting['value']> };
export const MockedSettingsContext = ({ settings, children }: MockedSettingsContextProps) => {
return <SettingsContext.Provider value={createSettingContextValue({ settings })}>{children}</SettingsContext.Provider>;
};

View File

@@ -0,0 +1,39 @@
import { UserContext } from '@rocket.chat/ui-contexts';
import type { ContextType, ReactNode } from 'react';
const userContextValue: ContextType<typeof UserContext> = {
userId: 'john.doe',
user: {
_id: 'john.doe',
username: 'john.doe',
name: 'John Doe',
createdAt: new Date(),
active: true,
_updatedAt: new Date(),
roles: ['admin'],
type: 'user',
},
queryPreference: (<T,>(pref: string, defaultValue: T) => [
() => () => undefined,
() => (typeof pref === 'string' ? undefined : defaultValue),
]) as any,
querySubscriptions: () => [() => () => undefined, () => []],
querySubscription: () => [() => () => undefined, () => undefined],
queryRoom: () => [() => () => undefined, () => undefined],
logout: () => Promise.resolve(),
onLogout: () => () => undefined,
};
const createUserContextValue = ({ userPreferences }: { userPreferences?: Record<string, unknown> }): ContextType<typeof UserContext> => {
return {
...userContextValue,
...(userPreferences && { queryPreference: (id) => [() => () => undefined, () => userPreferences[id as unknown as string] as any] }),
};
};
export type MockedUserContextProps = { children: ReactNode; userPreferences?: Record<string, unknown> };
export const MockedUserContext = ({ userPreferences, children }: MockedUserContextProps) => {
return <UserContext.Provider value={createUserContextValue({ userPreferences })}>{children}</UserContext.Provider>;
};

View File

@@ -0,0 +1,12 @@
import { MockedAppRootBuilder } from './MockedAppRootBuilder';
export const mockAppRoot = () => new MockedAppRootBuilder();
export * from './MockedRouterContext';
export * from './MockedAuthorizationContext';
export * from './MockedModalContext';
export * from './MockedServerContext';
export * from './MockedSettingsContext';
export * from './MockedUserContext';
export * from './MockedDeviceContext';
export * from './MockedAppRootBuilder';

View File

@@ -0,0 +1,13 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import { renderHook } from '@testing-library/react';
import { mockAppRoot } from '..';
describe('useSetting', () => {
it('should return settings from context', () => {
const { result } = renderHook(() => useSetting('asd'), {
wrapper: mockAppRoot().withSetting('asd', 'qwe').build(),
});
expect(result.current).toEqual('qwe');
});
});

View File

@@ -0,0 +1,38 @@
import { useSettings } from '@rocket.chat/ui-contexts';
import { renderHook } from '@testing-library/react';
import { mockAppRoot } from '..';
describe('useSettings', () => {
it('should return all settings', () => {
const query = {};
const { result } = renderHook(() => useSettings(query), {
wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(),
});
expect(result.current).toEqual([
{
_id: 'asd',
value: 'qwe',
},
{
_id: 'zxc',
value: 'rty',
},
]);
});
it('should return settings filtered by _id', () => {
const query = {
_id: ['asd'],
};
const { result } = renderHook(() => useSettings(query), {
wrapper: mockAppRoot().withSetting('asd', 'qwe').withSetting('zxc', 'rty').build(),
});
expect(result.current).toEqual([
{
_id: 'asd',
value: 'qwe',
},
]);
});
});

View File

@@ -0,0 +1,43 @@
import { faker } from '@faker-js/faker';
import type { IUser } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import { useUserPresence } from '@rocket.chat/ui-contexts';
import { renderHook } from '@testing-library/react';
import { mockAppRoot } from '..';
// TODO: this will live in `mock-providers` package
function createFakeUser(overrides?: Partial<IUser>): IUser {
return {
_id: faker.database.mongodbObjectId(),
_updatedAt: faker.date.recent(),
username: faker.internet.userName(),
name: faker.person.fullName(),
createdAt: faker.date.recent(),
roles: ['user'],
active: faker.datatype.boolean(),
type: 'user',
...overrides,
};
}
const SAMPLE_STATUS = 'Sample Status';
const user = createFakeUser({
active: true,
roles: ['admin'],
type: 'user',
statusText: 'Sample Status',
status: UserStatus.ONLINE,
});
describe('useUserPresence', () => {
it('should return presence from context', () => {
const { result } = renderHook(() => useUserPresence(user._id), {
wrapper: mockAppRoot().withUsers([user]).build(),
});
expect(result.current?.status).toEqual(UserStatus.ONLINE);
expect(result.current?.statusText).toEqual(SAMPLE_STATUS);
});
});

View File

@@ -0,0 +1,9 @@
{
"extends": "@rocket.chat/tsconfig/client.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.spec.ts"]
}