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
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:
1434
packages/mock-providers/CHANGELOG.md
Normal file
1434
packages/mock-providers/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
8
packages/mock-providers/jest.config.ts
Normal file
8
packages/mock-providers/jest.config.ts
Normal 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;
|
||||
53
packages/mock-providers/package.json
Normal file
53
packages/mock-providers/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
905
packages/mock-providers/src/MockedAppRootBuilder.tsx
Normal file
905
packages/mock-providers/src/MockedAppRootBuilder.tsx
Normal 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>;
|
||||
}
|
||||
}
|
||||
32
packages/mock-providers/src/MockedAuthorizationContext.tsx
Normal file
32
packages/mock-providers/src/MockedAuthorizationContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
31
packages/mock-providers/src/MockedDeviceContext.tsx
Normal file
31
packages/mock-providers/src/MockedDeviceContext.tsx
Normal 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>;
|
||||
};
|
||||
22
packages/mock-providers/src/MockedModalContext.tsx
Normal file
22
packages/mock-providers/src/MockedModalContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
28
packages/mock-providers/src/MockedRouterContext.tsx
Normal file
28
packages/mock-providers/src/MockedRouterContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
52
packages/mock-providers/src/MockedServerContext.tsx
Normal file
52
packages/mock-providers/src/MockedServerContext.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
36
packages/mock-providers/src/MockedSettingsContext.tsx
Normal file
36
packages/mock-providers/src/MockedSettingsContext.tsx
Normal 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>;
|
||||
};
|
||||
39
packages/mock-providers/src/MockedUserContext.tsx
Normal file
39
packages/mock-providers/src/MockedUserContext.tsx
Normal 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>;
|
||||
};
|
||||
12
packages/mock-providers/src/index.ts
Normal file
12
packages/mock-providers/src/index.ts
Normal 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';
|
||||
13
packages/mock-providers/src/tests/useSetting.spec.ts
Normal file
13
packages/mock-providers/src/tests/useSetting.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
38
packages/mock-providers/src/tests/useSettings.spec.ts
Normal file
38
packages/mock-providers/src/tests/useSettings.spec.ts
Normal 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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
43
packages/mock-providers/src/tests/useUserPresence.spec.ts
Normal file
43
packages/mock-providers/src/tests/useUserPresence.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
9
packages/mock-providers/tsconfig.json
Normal file
9
packages/mock-providers/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@rocket.chat/tsconfig/client.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./src/**/*"],
|
||||
"exclude": ["./src/**/*.spec.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user