Merge branch 'livekit' into renovate/livekit-client-2.x

This commit is contained in:
Robin
2024-02-21 08:50:01 -05:00
21 changed files with 824 additions and 1885 deletions

View File

@@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Log in to container registry - name: Log in to container registry
uses: docker/login-action@3d58c274f17dffee475a5520cbe67f0a882c4dbb uses: docker/login-action@83a00bc1ab5ded6580f31df1c49e6aaa932d840d
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -54,7 +54,7 @@ jobs:
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
- name: Upload - name: Upload
uses: actions/upload-artifact@4c0ff1c489dca52fedb26375d7d8fe7bd9233f19 uses: actions/upload-artifact@ef09cdac3e2d3e60d8ccadda691f4f1cec5035cb
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
with: with:

View File

@@ -1,11 +1,11 @@
name: Run jest tests name: Run unit tests
on: on:
pull_request: {} pull_request: {}
push: push:
branches: [livekit, full-mesh] branches: [livekit, full-mesh]
jobs: jobs:
jest: vitest:
name: Run jest tests name: Run vitest tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@@ -16,7 +16,7 @@ jobs:
cache: "yarn" cache: "yarn"
- name: Install dependencies - name: Install dependencies
run: "yarn install" run: "yarn install"
- name: Jest - name: Vitest
run: "yarn run test" run: "yarn run test"
- name: Upload to codecov - name: Upload to codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4

View File

@@ -13,7 +13,8 @@
"lint:types": "tsc", "lint:types": "tsc",
"i18n": "node_modules/i18next-parser/bin/cli.js", "i18n": "node_modules/i18next-parser/bin/cli.js",
"i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update", "i18n:check": "node_modules/i18next-parser/bin/cli.js --fail-on-warnings --fail-on-update",
"test": "jest", "test": "vitest",
"test:coverage": "vitest run --coverage",
"backend": "docker-compose -f backend-docker-compose.yml up" "backend": "docker-compose -f backend-docker-compose.yml up"
}, },
"dependencies": { "dependencies": {
@@ -88,21 +89,19 @@
"@react-spring/rafz": "^9.7.3", "@react-spring/rafz": "^9.7.3",
"@react-types/dialog": "^3.5.5", "@react-types/dialog": "^3.5.5",
"@sentry/vite-plugin": "^2.0.0", "@sentry/vite-plugin": "^2.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/content-type": "^1.1.5", "@types/content-type": "^1.1.5",
"@types/dom-screen-wake-lock": "^1.0.1", "@types/dom-screen-wake-lock": "^1.0.1",
"@types/dompurify": "^3.0.2", "@types/dompurify": "^3.0.2",
"@types/grecaptcha": "^3.0.4", "@types/grecaptcha": "^3.0.4",
"@types/jest": "^29.5.5",
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/react-router-dom": "^5.3.3", "@types/react-router-dom": "^5.3.3",
"@types/request": "^2.48.8", "@types/request": "^2.48.8",
"@types/sdp-transform": "^2.4.5", "@types/sdp-transform": "^2.4.5",
"@types/uuid": "9", "@types/uuid": "9",
"@typescript-eslint/eslint-plugin": "^6.1.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.1.0", "@typescript-eslint/parser": "^7.0.0",
"babel-loader": "^9.0.0", "babel-loader": "^9.0.0",
"babel-plugin-transform-vite-meta-env": "^1.0.3", "babel-plugin-transform-vite-meta-env": "^1.0.3",
"eslint": "^8.14.0", "eslint": "^8.14.0",
@@ -116,37 +115,14 @@
"eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-react-hooks": "^4.5.0",
"eslint-plugin-unicorn": "^51.0.0", "eslint-plugin-unicorn": "^51.0.0",
"i18next-parser": "^8.0.0", "i18next-parser": "^8.0.0",
"identity-obj-proxy": "^3.0.0", "jsdom": "^24.0.0",
"jest": "^29.2.2",
"jest-environment-jsdom": "^29.3.1",
"jest-mock": "^29.5.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"sass": "^1.42.1", "sass": "^1.42.1",
"typescript": "^5.1.6", "typescript": "^5.1.6",
"typescript-eslint-language-service": "^5.0.5", "typescript-eslint-language-service": "^5.0.5",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-plugin-html-template": "^1.1.0", "vite-plugin-html-template": "^1.1.0",
"vite-plugin-svgr": "^4.0.0" "vite-plugin-svgr": "^4.0.0",
}, "vitest": "^1.2.2"
"jest": {
"testEnvironment": "./test/environment.ts",
"testMatch": [
"<rootDir>/test/**/*-test.[jt]s?(x)"
],
"transformIgnorePatterns": [
"/node_modules/(?!d3)+$",
"/node_modules/(?!internmap)+$"
],
"moduleNameMapper": {
"\\.css$": "identity-obj-proxy",
"\\.svg\\?react$": "<rootDir>/test/mocks/svgr.ts",
"^\\./IndexedDBWorker\\?worker$": "<rootDir>/test/mocks/workerMock.ts",
"^\\./olm$": "<rootDir>/test/mocks/olmMock.ts"
},
"collectCoverage": true,
"coverageReporters": [
"text",
"cobertura"
]
} }
} }

View File

@@ -143,6 +143,7 @@
"unmute_microphone_button_label": "Unmute microphone", "unmute_microphone_button_label": "Unmute microphone",
"version": "Version: {{version}}", "version": "Version: {{version}}",
"video_tile": { "video_tile": {
"change_fit_contain": "Crop to fit",
"exit_full_screen": "Exit full screen", "exit_full_screen": "Exit full screen",
"full_screen": "Full screen", "full_screen": "Full screen",
"mute_for_me": "Mute for me", "mute_for_me": "Mute for me",

View File

@@ -188,16 +188,8 @@ export async function initClient(
await client.store.startup(); await client.store.startup();
} }
if (client.initCrypto) { await client.initCrypto();
await client.initCrypto(); await client.startClient();
}
await client.startClient({
// dirty hack to reduce chance of gappy syncs
// should be fixed by spotting gaps and backpaginating
initialSyncLimit: 50,
});
await waitForSync(client); await waitForSync(client);
return client; return client;

View File

@@ -41,6 +41,8 @@ export const RoomAuthView: FC = () => {
// @ts-ignore // @ts-ignore
(e) => { (e) => {
e.preventDefault(); e.preventDefault();
setLoading(true);
const data = new FormData(e.target); const data = new FormData(e.target);
const dataForDisplayName = data.get("displayName"); const dataForDisplayName = data.get("displayName");
const displayName = const displayName =

View File

@@ -167,6 +167,12 @@ export class UserMediaTileViewModel extends BaseTileViewModel {
*/ */
public readonly videoEnabled: StateObservable<boolean>; public readonly videoEnabled: StateObservable<boolean>;
private readonly _cropVideo = new BehaviorSubject(true);
/**
* Whether the tile video should be contained inside the tile or be cropped to fit.
*/
public readonly cropVideo = state(this._cropVideo);
public constructor( public constructor(
id: string, id: string,
member: RoomMember | undefined, member: RoomMember | undefined,
@@ -205,6 +211,10 @@ export class UserMediaTileViewModel extends BaseTileViewModel {
this._locallyMuted.next(!this._locallyMuted.value); this._locallyMuted.next(!this._locallyMuted.value);
} }
public toggleFitContain(): void {
this._cropVideo.next(!this._cropVideo.value);
}
public setLocalVolume(value: number): void { public setLocalVolume(value: number): void {
this._localVolume.next(value); this._localVolume.next(value);
} }

View File

@@ -73,7 +73,7 @@ borders don't support gradients */
.videoTile video { .videoTile video {
inline-size: 100%; inline-size: 100%;
block-size: 100%; block-size: 100%;
object-fit: cover; object-fit: contain;
background-color: var(--cpd-color-bg-subtle-primary); background-color: var(--cpd-color-bg-subtle-primary);
/* This transform is a no-op, but it forces Firefox to use a different /* This transform is a no-op, but it forces Firefox to use a different
rendering path, one that actually clips the corners of <video> elements into rendering path, one that actually clips the corners of <video> elements into
@@ -89,6 +89,10 @@ borders don't support gradients */
object-fit: contain; object-fit: contain;
} }
.videoTile.cropVideo video {
object-fit: cover;
}
.videoTile.videoMuted video { .videoTile.videoMuted video {
display: none; display: none;
} }

View File

@@ -206,9 +206,16 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
const mirror = useStateObservable(vm.mirror); const mirror = useStateObservable(vm.mirror);
const speaking = useStateObservable(vm.speaking); const speaking = useStateObservable(vm.speaking);
const locallyMuted = useStateObservable(vm.locallyMuted); const locallyMuted = useStateObservable(vm.locallyMuted);
const cropVideo = useStateObservable(vm.cropVideo);
const localVolume = useStateObservable(vm.localVolume); const localVolume = useStateObservable(vm.localVolume);
const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]); const onChangeMute = useCallback(() => vm.toggleLocallyMuted(), [vm]);
const onChangeFitContain = useCallback(() => vm.toggleFitContain(), [vm]);
const onSelectMute = useCallback((e: Event) => e.preventDefault(), []); const onSelectMute = useCallback((e: Event) => e.preventDefault(), []);
const onSelectFitContain = useCallback(
(e: Event) => e.preventDefault(),
[],
);
const onChangeLocalVolume = useCallback( const onChangeLocalVolume = useCallback(
(v: number) => vm.setLocalVolume(v), (v: number) => vm.setLocalVolume(v),
[vm], [vm],
@@ -225,6 +232,13 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
label={t("common.profile")} label={t("common.profile")}
onSelect={onOpenProfile} onSelect={onOpenProfile}
/> />
<ToggleMenuItem
Icon={ExpandIcon}
label={t("video_tile.change_fit_contain")}
checked={cropVideo}
onChange={onChangeFitContain}
onSelect={onSelectFitContain}
/>
</> </>
) : ( ) : (
<> <>
@@ -235,6 +249,13 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
onChange={onChangeMute} onChange={onChangeMute}
onSelect={onSelectMute} onSelect={onSelectMute}
/> />
<ToggleMenuItem
Icon={ExpandIcon}
label={t("video_tile.change_fit_contain")}
checked={cropVideo}
onChange={onChangeFitContain}
onSelect={onSelectFitContain}
/>
{/* TODO: Figure out how to make this slider keyboard accessible */} {/* TODO: Figure out how to make this slider keyboard accessible */}
<MenuItem as="div" Icon={VolumeIcon} label={null} onSelect={null}> <MenuItem as="div" Icon={VolumeIcon} label={null} onSelect={null}>
<Slider <Slider
@@ -257,6 +278,7 @@ const UserMediaTile = subscribe<UserMediaTileProps, HTMLDivElement>(
className={classNames(className, { className={classNames(className, {
[styles.mirror]: mirror, [styles.mirror]: mirror,
[styles.speaking]: showSpeakingIndicator && speaking, [styles.speaking]: showSpeakingIndicator && speaking,
[styles.cropVideo]: cropVideo,
})} })}
style={style} style={style}
targetWidth={targetWidth} targetWidth={targetWidth}

View File

@@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { vi } from "vitest";
import { screen, render } from "@testing-library/react"; import { screen, render } from "@testing-library/react";
import { Toast } from "../src/Toast"; import { Toast } from "../src/Toast";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
@@ -35,7 +36,7 @@ test("Toast renders", () => {
}); });
test("Toast dismisses when clicked", async () => { test("Toast dismisses when clicked", async () => {
const onDismiss = jest.fn(); const onDismiss = vi.fn();
render( render(
<Toast open={true} onDismiss={onDismiss}> <Toast open={true} onDismiss={onDismiss}>
Hello world! Hello world!
@@ -47,13 +48,13 @@ test("Toast dismisses when clicked", async () => {
test("Toast dismisses itself after the specified timeout", async () => { test("Toast dismisses itself after the specified timeout", async () => {
withFakeTimers(() => { withFakeTimers(() => {
const onDismiss = jest.fn(); const onDismiss = vi.fn();
render( render(
<Toast open={true} onDismiss={onDismiss} autoDismiss={2000}> <Toast open={true} onDismiss={onDismiss} autoDismiss={2000}>
Hello world! Hello world!
</Toast>, </Toast>,
); );
jest.advanceTimersByTime(2000); vi.advanceTimersByTime(2000);
expect(onDismiss).toHaveBeenCalled(); expect(onDismiss).toHaveBeenCalled();
}); });
}); });

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { mocked } from "jest-mock"; import { vi } from "vitest";
import { getRoomIdentifierFromUrl } from "../src/UrlParams"; import { getRoomIdentifierFromUrl } from "../src/UrlParams";
import { Config } from "../src/config/Config"; import { Config } from "../src/config/Config";
@@ -24,11 +24,11 @@ const ROOM_ID = "!d45f138fsd";
const ORIGIN = "https://call.element.io"; const ORIGIN = "https://call.element.io";
const HOMESERVER = "call.ems.host"; const HOMESERVER = "call.ems.host";
jest.mock("../src/config/Config"); vi.mock("../src/config/Config");
describe("UrlParams", () => { describe("UrlParams", () => {
beforeAll(() => { beforeAll(() => {
mocked(Config.defaultServerName).mockReturnValue("call.ems.host"); vi.mocked(Config.defaultServerName).mockReturnValue("call.ems.host");
}); });
describe("handles URL with /room/", () => { describe("handles URL with /room/", () => {

View File

@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Toast renders 1`] = ` exports[`Toast renders 1`] = `
<button <button

View File

@@ -1,18 +0,0 @@
import { TextEncoder } from "util";
import JSDOMEnvironment_, {
TestEnvironment as TestEnvironment_,
} from "jest-environment-jsdom";
import { JestEnvironmentConfig, EnvironmentContext } from "@jest/environment";
// This is a patched version of jsdom that adds TextEncoder, as a workaround for
// https://github.com/jsdom/jsdom/issues/2524
// Once that issue is resolved, this custom environment file can be deleted
export default class JSDOMEnvironment extends JSDOMEnvironment_ {
constructor(config: JestEnvironmentConfig, context: EnvironmentContext) {
super(config, context);
this.global.TextEncoder ??= TextEncoder;
}
}
export const TestEnvironment =
TestEnvironment_ === JSDOMEnvironment_ ? JSDOMEnvironment : TestEnvironment_;

View File

@@ -1 +0,0 @@
module.exports = { loadOlm: jest.fn(async () => {}) };

View File

@@ -1,3 +0,0 @@
// Mock file for SVG imports
const ReactComponent = "svg";
export default ReactComponent;

View File

@@ -1 +0,0 @@
module.exports = jest.fn();

View File

@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { Mocked, mocked } from "jest-mock"; import { vi, Mocked } from "vitest";
import { RoomState } from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics"; import { PosthogAnalytics } from "../../src/analytics/PosthogAnalytics";
import { checkForParallelCalls } from "../../src/room/checkForParallelCalls"; import { checkForParallelCalls } from "../../src/room/checkForParallelCalls";
@@ -23,10 +23,10 @@ import { withFakeTimers } from "../utils";
const withMockedPosthog = ( const withMockedPosthog = (
continuation: (posthog: Mocked<PosthogAnalytics>) => void, continuation: (posthog: Mocked<PosthogAnalytics>) => void,
) => { ) => {
const posthog = mocked({ const posthog = vi.mocked({
trackEvent: jest.fn(), trackEvent: vi.fn(),
} as unknown as PosthogAnalytics); } as unknown as PosthogAnalytics);
const instanceSpy = jest const instanceSpy = vi
.spyOn(PosthogAnalytics, "instance", "get") .spyOn(PosthogAnalytics, "instance", "get")
.mockReturnValue(posthog); .mockReturnValue(posthog);
try { try {

View File

@@ -13,12 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { vi } from "vitest";
export function withFakeTimers(continuation: () => void): void { export function withFakeTimers(continuation: () => void): void {
jest.useFakeTimers(); vi.useFakeTimers();
try { try {
continuation(); continuation();
} finally { } finally {
jest.useRealTimers(); vi.useRealTimers();
} }
} }

View File

@@ -12,9 +12,23 @@
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true, "esModuleInterop": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"declaration": true, "declaration": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"paths": {
// These imports within @livekit/components-core and
// @livekit/components-react are broken under the "bundler" module
// resolution mode, so we need to resolve them manually
"livekit-client/dist/src/room/Room": [
"./node_modules/livekit-client/dist/src/room/Room.d.ts"
],
"livekit-client/dist/src/room/participant/Participant": [
"./node_modules/livekit-client/dist/src/room/participant/Participant.d.ts"
],
"livekit-client/dist/src/proto/livekit_models_pb": [
"./node_modules/livekit-client/dist/src/proto/livekit_models_pb.d.ts"
]
},
// TODO: Enable the following options later. // TODO: Enable the following options later.
// "forceConsistentCasingInFileNames": true, // "forceConsistentCasingInFileNames": true,
@@ -25,13 +39,14 @@
// "noUncheckedIndexedAccess": true, // "noUncheckedIndexedAccess": true,
// "noUnusedParameters": true, // "noUnusedParameters": true,
"plugins": [{ "name": "typescript-eslint-language-service" }], "plugins": [{ "name": "typescript-eslint-language-service" }]
}, },
"include": [ "include": [
"./node_modules/matrix-js-sdk/src/@types/*.d.ts", "./node_modules/matrix-js-sdk/src/@types/*.d.ts",
"./node_modules/vitest/globals.d.ts",
"./src/**/*.ts", "./src/**/*.ts",
"./src/**/*.tsx", "./src/**/*.tsx",
"./test/**/*.ts", "./test/**/*.ts",
"./test/**/*.tsx", "./test/**/*.tsx"
], ]
} }

24
vitest.config.js Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig, mergeConfig } from "vitest/config";
import viteConfig from "./vite.config";
export default defineConfig((configEnv) =>
mergeConfig(
viteConfig(configEnv),
defineConfig({
test: {
globals: true,
environment: "jsdom",
css: {
modules: {
classNameStrategy: "non-scoped",
},
},
include: ["test/**/*-test.[jt]s?(x)"],
coverage: {
reporter: ["text", "html"],
exclude: ["node_modules/"],
},
},
}),
),
);

2508
yarn.lock

File diff suppressed because it is too large Load Diff