From ff615ea9f2adbbc76b00365f863112cb6c252ab2 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 22 Dec 2025 15:17:21 -0300 Subject: [PATCH 001/104] chore(sha256): Review build configuration (#37923) --- packages/sha256/.eslintrc.json | 4 ---- packages/sha256/babel.config.js | 3 --- packages/sha256/package.json | 4 ---- packages/sha256/tsconfig.build.json | 3 ++- packages/sha256/tsconfig.json | 2 -- yarn.lock | 6 +----- 6 files changed, 3 insertions(+), 19 deletions(-) delete mode 100644 packages/sha256/babel.config.js diff --git a/packages/sha256/.eslintrc.json b/packages/sha256/.eslintrc.json index f27fe59dc9222..9ec331f09b5e3 100644 --- a/packages/sha256/.eslintrc.json +++ b/packages/sha256/.eslintrc.json @@ -1,8 +1,4 @@ { "extends": ["@rocket.chat/eslint-config"], - "plugins": ["jest"], - "env": { - "jest/globals": true - }, "ignorePatterns": ["dist"] } diff --git a/packages/sha256/babel.config.js b/packages/sha256/babel.config.js deleted file mode 100644 index 5230bd1a4356a..0000000000000 --- a/packages/sha256/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ['@babel/preset-env'], -}; diff --git a/packages/sha256/package.json b/packages/sha256/package.json index eb52d68d3789c..3e780df716cb4 100644 --- a/packages/sha256/package.json +++ b/packages/sha256/package.json @@ -15,13 +15,9 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { - "@babel/core": "~7.28.5", - "@babel/preset-env": "~7.28.5", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", "eslint": "~8.45.0", "jest": "~30.2.0", "typescript": "~5.9.3" diff --git a/packages/sha256/tsconfig.build.json b/packages/sha256/tsconfig.build.json index fb44f1fb1e599..f60fb5fec5a45 100644 --- a/packages/sha256/tsconfig.build.json +++ b/packages/sha256/tsconfig.build.json @@ -1,11 +1,12 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "outDir": "dist", "target": "ES2015", "module": "commonjs", "declaration": true, "declarationMap": true, "sourceMap": true, }, - "exclude": ["**/*.spec.ts", "**/*.spec.js"] + "exclude": ["**/*.spec.ts"] } diff --git a/packages/sha256/tsconfig.json b/packages/sha256/tsconfig.json index 20d11edcf6a8f..6918f7d88357c 100644 --- a/packages/sha256/tsconfig.json +++ b/packages/sha256/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "@rocket.chat/tsconfig/base.json", "compilerOptions": { - "outDir": "dist", "rootDir": "src", - "strictNullChecks": true, }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index b734ffabcc05c..939a8ac0b60c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10237,13 +10237,9 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/sha256@workspace:packages/sha256" dependencies: - "@babel/core": "npm:~7.28.5" - "@babel/preset-env": "npm:~7.28.5" "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" @@ -10797,7 +10793,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.3 - "@rocket.chat/ui-contexts": 25.0.1 + "@rocket.chat/ui-contexts": 25.0.2 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From cfea98d2304d1ce47df05f77423a501b95acaf1a Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 22 Dec 2025 15:17:33 -0300 Subject: [PATCH 002/104] chore(presence): Review build configuration (#37926) --- ee/packages/presence/.eslintrc.json | 8 -------- ee/packages/presence/babel.config.js | 3 --- ee/packages/presence/package.json | 10 +++------- ee/packages/presence/tsconfig.json | 1 - yarn.lock | 4 ---- 5 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 ee/packages/presence/babel.config.js diff --git a/ee/packages/presence/.eslintrc.json b/ee/packages/presence/.eslintrc.json index c36ef5941c077..9ec331f09b5e3 100644 --- a/ee/packages/presence/.eslintrc.json +++ b/ee/packages/presence/.eslintrc.json @@ -1,12 +1,4 @@ { "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], "ignorePatterns": ["dist"] } diff --git a/ee/packages/presence/babel.config.js b/ee/packages/presence/babel.config.js deleted file mode 100644 index 7672dadf24ca2..0000000000000 --- a/ee/packages/presence/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], -}; diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index 93f1c4ea46a7f..4fe9be3cbac0f 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -9,11 +9,11 @@ ], "scripts": { "build": "tsc", - "lint": "eslint src", - "lint:fix": "eslint src --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", - "typecheck": "tsc --noEmit --skipLibCheck" + "typecheck": "tsc --noEmit" }, "dependencies": { "@rocket.chat/core-services": "workspace:^", @@ -22,14 +22,10 @@ "mongodb": "6.16.0" }, "devDependencies": { - "@babel/core": "~7.28.5", - "@babel/preset-env": "~7.28.5", - "@babel/preset-typescript": "~7.27.1", "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "~22.16.5", - "babel-jest": "~30.2.0", "eslint": "~8.45.0", "jest": "~30.2.0", "typescript": "~5.9.3" diff --git a/ee/packages/presence/tsconfig.json b/ee/packages/presence/tsconfig.json index 11bd7de3d9e61..a5fa14fc6913e 100644 --- a/ee/packages/presence/tsconfig.json +++ b/ee/packages/presence/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "@rocket.chat/tsconfig/server.json", "compilerOptions": { - "strictPropertyInitialization": false, // TODO: Remove this line "declaration": true, "outDir": "./dist", "rootDir": "./src", diff --git a/yarn.lock b/yarn.lock index 939a8ac0b60c9..d44f505e8d05d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10079,9 +10079,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/presence@workspace:ee/packages/presence" dependencies: - "@babel/core": "npm:~7.28.5" - "@babel/preset-env": "npm:~7.28.5" - "@babel/preset-typescript": "npm:~7.27.1" "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" @@ -10089,7 +10086,6 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@types/node": "npm:~22.16.5" - babel-jest: "npm:~30.2.0" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" From 0f6159d407f25c83f4fbd475b73a4c7139aa5706 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Mon, 22 Dec 2025 15:17:49 -0300 Subject: [PATCH 003/104] chore(core-services): Review build configuration (#37925) --- packages/core-services/.eslintrc.json | 8 -------- packages/core-services/babel.config.js | 3 --- packages/core-services/package.json | 9 +++------ yarn.lock | 4 ---- 4 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 packages/core-services/babel.config.js diff --git a/packages/core-services/.eslintrc.json b/packages/core-services/.eslintrc.json index c36ef5941c077..9ec331f09b5e3 100644 --- a/packages/core-services/.eslintrc.json +++ b/packages/core-services/.eslintrc.json @@ -1,12 +1,4 @@ { "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], "ignorePatterns": ["dist"] } diff --git a/packages/core-services/babel.config.js b/packages/core-services/babel.config.js deleted file mode 100644 index 7672dadf24ca2..0000000000000 --- a/packages/core-services/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], -}; diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 7dd8ae4c729c0..a26a13480d35b 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -10,8 +10,9 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" }, @@ -28,15 +29,11 @@ "@rocket.chat/ui-kit": "workspace:~" }, "devDependencies": { - "@babel/core": "~7.28.5", - "@babel/preset-env": "~7.28.5", - "@babel/preset-typescript": "~7.27.1", "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "babel-jest": "~30.2.0", "eslint": "~8.45.0", "jest": "~30.2.0", "mongodb": "6.16.0", diff --git a/yarn.lock b/yarn.lock index d44f505e8d05d..68da17da42e55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8461,9 +8461,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/core-services@workspace:packages/core-services" dependencies: - "@babel/core": "npm:~7.28.5" - "@babel/preset-env": "npm:~7.28.5" - "@babel/preset-typescript": "npm:~7.27.1" "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" @@ -8479,7 +8476,6 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-kit": "workspace:~" "@types/jest": "npm:~30.0.0" - babel-jest: "npm:~30.2.0" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" From d7924d386f23673ad49ff1c1c78b23a7b1672b4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 15:25:30 -0300 Subject: [PATCH 004/104] chore(deps): bump actions/upload-artifact from 5 to 6 (#37917) --- .github/workflows/ci-test-e2e.yml | 4 ++-- .github/workflows/ci.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index bb09691b174bc..bddf9d3233e74 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -248,7 +248,7 @@ jobs: - name: Store playwright test trace if: inputs.type == 'ui' && always() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }}${{ inputs.db-watcher-disabled == 'true' && '-no-watcher' || '' }} path: ./apps/meteor/tests/e2e/.playwright* @@ -264,7 +264,7 @@ jobs: - name: Store coverage if: inputs.coverage == matrix.mongodb-version - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: coverage-${{ inputs.type }}-${{ matrix.shard }} path: /tmp/coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 738fa87fb685c..c9e98f02a7bc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -220,7 +220,7 @@ jobs: $(git ls-files -oi --exclude-standard -- ':(exclude)node_modules/*' ':(exclude)**/node_modules/*' ':(exclude)**/.meteor/*' ':(exclude)**/.turbo/*' ':(exclude).turbo/*' ':(exclude)**/.yarn/*' ':(exclude).yarn/*' ':(exclude).git/*') - name: Upload packages build artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: packages-build path: /tmp/RocketChat-packages-build.tar.gz @@ -228,7 +228,7 @@ jobs: - name: Store turbo build if: steps.packages-cache-build.outputs.cache-hit != 'true' - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: turbo-build path: .turbo/cache @@ -657,7 +657,7 @@ jobs: npx nyc report --reporter=lcovonly --report-dir=/tmp/coverage_report/ui --temp-dir=/tmp/coverage/ui - name: Store coverage-reports - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: reports-coverage path: /tmp/coverage_report From d48c635a4e7007041981d35a9793aeacde406a52 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Dec 2025 21:43:58 -0300 Subject: [PATCH 005/104] chore(ci): update coverage configuration in CI workflow (#37931) --- .github/workflows/ci-test-e2e.yml | 2 -- .github/workflows/ci.yml | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index bddf9d3233e74..7b314ada7c290 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -174,8 +174,6 @@ jobs: env: ENTERPRISE_LICENSE: ${{ inputs.enterprise-license }} TRANSPORTER: ${{ inputs.transporter }} - COVERAGE_DIR: '/tmp/coverage' - COVERAGE_REPORTER: 'lcov' run: | DEBUG_LOG_LEVEL=${DEBUG_LOG_LEVEL:-0} docker compose -f docker-compose-ci.yml up -d --wait diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9e98f02a7bc7..05c6a858b35d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -566,6 +566,7 @@ jobs: shard: '[1, 2, 3, 4, 5]' total-shard: 5 mongodb-version: "['8.2']" + coverage: '8.2' node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} From 398c458490da00984ddf390f9fe1221ae31b86e1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 22 Dec 2025 23:26:05 -0300 Subject: [PATCH 006/104] chore: update package versions to 8.1.0-develop --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index 50842259a0b86..9233762035673 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.0.0-rc.1" + "version": "8.1.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5215ad149a265..d5f09f8d4b1a6 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/meteor", - "version": "8.0.0-rc.1", + "version": "8.1.0-develop", "private": true, "description": "The Ultimate Open Source WebChat Platform", "keywords": [ diff --git a/package.json b/package.json index e5797ee34931e..1b15fe80184e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "8.0.0-rc.1", + "version": "8.1.0-develop", "private": true, "description": "Rocket.Chat Monorepo", "homepage": "https://github.com/RocketChat/Rocket.Chat#readme", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index e6077970a3916..b4521f251e56e 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "8.0.0-rc.1", + "version": "8.1.0-develop", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 3f60c85e7f28c..3cc07b2179859 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "8.0.0-rc.1", + "version": "8.1.0-develop", "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ From 60d2019908e21e6269e497c583b63661114cac69 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 23 Dec 2025 00:36:18 -0300 Subject: [PATCH 007/104] chore: update version check for user audit deprecation handling --- .../meteor/app/lib/server/functions/saveUser/saveUser.ts | 9 ++++----- apps/meteor/server/lib/shouldBreakInVersion.ts | 4 +++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index 6665177794e73..b727867b1e818 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -235,16 +235,15 @@ const _saveUser = (session?: ClientSession) => return true; }; -const isBroken = shouldBreakInVersion('8.0.0'); +const isBroken = shouldBreakInVersion('9.0.0'); export const saveUser = (() => { - if (isBroken) { - throw new Error('DEBUG_DISABLE_USER_AUDIT flag is deprecated and should be removed'); - } - if (!process.env.DEBUG_DISABLE_USER_AUDIT) { return wrapInSessionTransaction(_saveUser); } + if (isBroken) { + throw new Error('DEBUG_DISABLE_USER_AUDIT flag is deprecated and should be removed'); + } const saveUserNoSession = _saveUser(); return function saveUser(userId: IUser['_id'], userData: SaveUserData, _options?: any) { return saveUserNoSession(userId, userData); diff --git a/apps/meteor/server/lib/shouldBreakInVersion.ts b/apps/meteor/server/lib/shouldBreakInVersion.ts index 2ba8c71c31ee4..ae41fd42c2b24 100644 --- a/apps/meteor/server/lib/shouldBreakInVersion.ts +++ b/apps/meteor/server/lib/shouldBreakInVersion.ts @@ -1,5 +1,7 @@ import semver from 'semver'; +import type { DeprecationLoggerNextPlannedVersion } from '../../app/lib/server/lib/deprecationWarningLogger'; import { Info } from '../../app/utils/rocketchat.info'; -export const shouldBreakInVersion = (version: string) => semver.gte(Info.version, version); +export const shouldBreakInVersion = (version: DeprecationLoggerNextPlannedVersion) => + Boolean(semver.gte(Info.version, version) && !process.env.TEST_MODE); From 5be5362c468d6682175ca434ff7ce7d6a274a4ff Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 23 Dec 2025 00:37:51 -0300 Subject: [PATCH 008/104] chore: update api breaking changes version check to 9.0.0 --- apps/meteor/app/api/server/ApiClass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index 895bd659af326..60af03b031fdc 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -56,7 +56,7 @@ const logger = new Logger('API'); // We have some breaking changes planned to the API. // To avoid conflicts or missing something during the period we are adopting a 'feature flag approach' // TODO: MAJOR check if this is still needed -const applyBreakingChanges = shouldBreakInVersion('8.0.0'); +const applyBreakingChanges = shouldBreakInVersion('9.0.0'); type MinimalRoute = { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: string; From a8a84abe73f35b7c77554345509d36d044aa3847 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 23 Dec 2025 09:53:46 -0600 Subject: [PATCH 009/104] chore: InitialData & serverRunning to TS (#37929) --- .../definition/externals/meteor/meteor.d.ts | 2 +- .../{initialData.js => initialData.ts} | 61 +++++++++---------- .../{serverRunning.js => serverRunning.ts} | 20 ++++-- .../model-typings/src/models/IRoomsModel.ts | 2 +- packages/models/src/models/Rooms.ts | 2 +- 5 files changed, 47 insertions(+), 40 deletions(-) rename apps/meteor/server/startup/{initialData.js => initialData.ts} (83%) rename apps/meteor/server/startup/{serverRunning.js => serverRunning.ts} (87%) diff --git a/apps/meteor/definition/externals/meteor/meteor.d.ts b/apps/meteor/definition/externals/meteor/meteor.d.ts index a465ad3687b46..a52e0fec2b633 100644 --- a/apps/meteor/definition/externals/meteor/meteor.d.ts +++ b/apps/meteor/definition/externals/meteor/meteor.d.ts @@ -8,7 +8,7 @@ type StringifyBuffers = { declare global { namespace Assets { - function getBinaryAsync(assetPath: string): Promise; + function getBinaryAsync(assetPath: string): Promise; function getTextAsync(assetPath: string): Promise; } diff --git a/apps/meteor/server/startup/initialData.js b/apps/meteor/server/startup/initialData.ts similarity index 83% rename from apps/meteor/server/startup/initialData.js rename to apps/meteor/server/startup/initialData.ts index 7d7a9fdec96d2..be73e6ea2c79d 100644 --- a/apps/meteor/server/startup/initialData.js +++ b/apps/meteor/server/startup/initialData.ts @@ -1,3 +1,4 @@ +import { UserStatus, type IUser } from '@rocket.chat/core-typings'; import { Settings, Rooms, Users, Roles } from '@rocket.chat/models'; import { validateEmail } from '@rocket.chat/tools'; import colors from 'colors/safe'; @@ -16,19 +17,16 @@ import { addUserRolesAsync } from '../lib/roles/addUserRoles'; export async function insertAdminUserFromEnv() { if (process.env.ADMIN_PASS) { if ((await Roles.countUsersInRole('admin')) === 0) { - const adminUser = { - name: 'Administrator', + const adminUser: Partial = { + name: process.env.ADMIN_NAME || 'Administrator', username: 'admin', - status: 'offline', - statusDefault: 'online', + status: UserStatus.OFFLINE, + statusDefault: UserStatus.ONLINE, utcOffset: 0, active: true, + type: 'user', }; - if (process.env.ADMIN_NAME) { - adminUser.name = process.env.ADMIN_NAME; - } - console.log(colors.green(`Name: ${adminUser.name}`)); if (process.env.ADMIN_EMAIL) { @@ -75,8 +73,6 @@ export async function insertAdminUserFromEnv() { console.log(colors.green(`Username: ${adminUser.username}`)); - adminUser.type = 'user'; - const { insertedId: userId } = await Users.create(adminUser); await Accounts.setPasswordAsync(userId, process.env.ADMIN_PASS); @@ -137,8 +133,8 @@ Meteor.startup(async () => { _id: 'rocket.cat', name: 'Rocket.Cat', username: 'rocket.cat', - status: 'online', - statusDefault: 'online', + status: UserStatus.ONLINE, + statusDefault: UserStatus.ONLINE, utcOffset: 0, active: true, type: 'bot', @@ -146,20 +142,23 @@ Meteor.startup(async () => { await addUserRolesAsync('rocket.cat', ['bot']); - const buffer = Buffer.from(await Assets.getBinaryAsync('avatars/rocketcat.png')); + const asset = await Assets.getBinaryAsync('avatars/rocketcat.png'); + if (asset) { + const buffer = Buffer.from(asset); - const rs = RocketChatFile.bufferToStream(buffer, 'utf8'); - const fileStore = FileUpload.getStore('Avatars'); - await fileStore.deleteByName('rocket.cat'); + const rs = RocketChatFile.bufferToStream(buffer); + const fileStore = FileUpload.getStore('Avatars'); + await fileStore.deleteByName('rocket.cat'); - const file = { - userId: 'rocket.cat', - type: 'image/png', - size: buffer.length, - }; + const file = { + userId: 'rocket.cat', + type: 'image/png', + size: buffer.length, + }; - const upload = await fileStore.insert(file, rs); - await Users.setAvatarData('rocket.cat', 'local', upload.etag); + const upload = await fileStore.insert(file, rs); + await Users.setAvatarData('rocket.cat', 'local', upload.etag); + } } } catch (error) { console.log( @@ -211,7 +210,7 @@ Meteor.startup(async () => { if (process.env.TEST_MODE === 'true') { console.log(colors.green('Inserting admin test user:')); - const adminUser = { + const adminUser: Omit = { _id: 'rocketchat.internal.admin.test', name: 'RocketChat Internal Admin Test', username: 'rocketchat.internal.admin.test', @@ -221,23 +220,23 @@ Meteor.startup(async () => { verified: true, }, ], - status: 'offline', - statusDefault: 'online', + status: UserStatus.OFFLINE, + statusDefault: UserStatus.ONLINE, utcOffset: 0, active: true, type: 'user', }; console.log(colors.green(`Name: ${adminUser.name}`)); - console.log(colors.green(`Email: ${adminUser.emails[0].address}`)); + console.log(colors.green(`Email: ${adminUser.emails![0].address}`)); console.log(colors.green(`Username: ${adminUser.username}`)); console.log(colors.green(`Password: ${adminUser._id}`)); - if (await Users.findOneByEmailAddress(adminUser.emails[0].address)) { - throw new Meteor.Error(`Email ${adminUser.emails[0].address} already exists`, "Rocket.Chat can't run in test mode"); + if (await Users.findOneByEmailAddress(adminUser.emails![0].address)) { + throw new Meteor.Error(`Email ${adminUser.emails![0].address} already exists`, "Rocket.Chat can't run in test mode"); } - if (!(await checkUsernameAvailability(adminUser.username))) { + if (!(await checkUsernameAvailability(adminUser.username!))) { throw new Meteor.Error(`Username ${adminUser.username} already exists`, "Rocket.Chat can't run in test mode"); } @@ -252,7 +251,7 @@ Meteor.startup(async () => { void notifyOnSettingChangedById('Show_Setup_Wizard'); } - await addUserToDefaultChannels(adminUser, true); + await addUserToDefaultChannels(adminUser as IUser, true); // Create sample call history for API tests return addCallHistoryTestData('rocketchat.internal.admin.test', 'rocket.cat'); diff --git a/apps/meteor/server/startup/serverRunning.js b/apps/meteor/server/startup/serverRunning.ts similarity index 87% rename from apps/meteor/server/startup/serverRunning.js rename to apps/meteor/server/startup/serverRunning.ts index 9aa0c00e769da..3bb9433ecc4fd 100644 --- a/apps/meteor/server/startup/serverRunning.js +++ b/apps/meteor/server/startup/serverRunning.ts @@ -13,7 +13,7 @@ import { getMongoInfo } from '../../app/utils/server/functions/getMongoInfo'; // import { sendMessagesToAdmins } from '../lib/sendMessagesToAdmins'; import { showErrorBox, showSuccessBox } from '../lib/logger/showBox'; -const exitIfNotBypassed = (ignore, errorCode = 1) => { +const exitIfNotBypassed = (ignore: string | undefined, errorCode = 1) => { if (typeof ignore === 'string' && ['yes', 'true'].includes(ignore.toLowerCase())) { return; } @@ -28,10 +28,16 @@ Meteor.startup(async () => { const { mongoVersion, mongoStorageEngine } = await getMongoInfo(); const desiredNodeVersion = semver.clean(fs.readFileSync(path.join(process.cwd(), '../../.node_version.txt')).toString()); - const desiredNodeVersionMajor = String(semver.parse(desiredNodeVersion).major); + const parsedSemVer = semver.parse(desiredNodeVersion); + if (!parsedSemVer) { + console.error('Failed to parse desired Node.js version from .node_version.txt'); + process.exit(1); + } + + const desiredNodeVersionMajor = String(parsedSemVer.major); return setTimeout(async () => { - let msg = [ + let msg: string | string[] = [ `Rocket.Chat Version: ${Info.version}`, ` NodeJS Version: ${process.versions.node} - ${process.arch}`, ` MongoDB Version: ${mongoVersion}`, @@ -41,11 +47,11 @@ Meteor.startup(async () => { ` Site URL: ${settings.get('Site_Url')}`, ]; - if (Info.commit && Info.commit.hash) { + if (Info.commit?.hash) { msg.push(` Commit Hash: ${Info.commit.hash.substr(0, 10)}`); } - if (Info.commit && Info.commit.branch) { + if (Info.commit?.branch) { msg.push(` Commit Branch: ${Info.commit.branch}`); } @@ -63,7 +69,9 @@ Meteor.startup(async () => { exitIfNotBypassed(process.env.BYPASS_NODEJS_VALIDATION); } - if (semver.satisfies(semver.coerce(mongoVersion), '<7.0.0')) { + const mongoSemver = semver.coerce(mongoVersion); + + if (!mongoSemver || semver.satisfies(mongoSemver, '<7.0.0')) { msg += ['', '', 'YOUR CURRENT MONGODB VERSION IS NOT SUPPORTED BY ROCKET.CHAT,', 'PLEASE UPGRADE TO VERSION 7.0 OR LATER'].join('\n'); showErrorBox('SERVER ERROR', msg); diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 5a89147e47220..2639f6adcb3a7 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -299,7 +299,7 @@ export interface IRoomsModel extends IBaseModel { saveRetentionOverrideGlobalById(rid: string, retentionOverrideGlobal: boolean): Promise; saveEncryptedById(rid: string, encrypted: boolean): Promise; updateGroupDMsRemovingUsernamesByUsername(username: string, userId: string): Promise; - createWithIdTypeAndName(id: string, type: IRoom['t'], name: string, extraData?: Record): Promise; + createWithIdTypeAndName(id: string, type: IRoom['t'], name: string, extraData?: Record): Promise; createWithFullRoomData(room: Omit): Promise; removeById(rid: string): Promise; removeByIds(rids: string[]): Promise; diff --git a/packages/models/src/models/Rooms.ts b/packages/models/src/models/Rooms.ts index 81edc91ae703e..9bcac18cf72fe 100644 --- a/packages/models/src/models/Rooms.ts +++ b/packages/models/src/models/Rooms.ts @@ -2041,7 +2041,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { _id: IRoom['_id'], type: IRoom['t'], name: IRoom['name'], - extraData?: Record, + extraData?: Record, ): Promise { const room: IRoom = { _id, From 108f7f51851c5b515bddb826cb65351aff71ed0a Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 24 Dec 2025 16:11:26 -0300 Subject: [PATCH 010/104] test: Reorg modal page objects (#37871) --- .../omnichannel/modals/ForwardChatModal.tsx | 2 +- apps/meteor/tests/e2e/apps/apps-modal.spec.ts | 2 +- .../e2ee-encryption-decryption.spec.ts | 2 +- .../e2ee-passphrase-management.spec.ts | 8 +- .../e2e/e2e-encryption/setupE2EEPassword.ts | 3 +- apps/meteor/tests/e2e/feature-preview.spec.ts | 2 +- .../e2e/federation/page-objects/channel.ts | 2 +- .../federation/tests/ce-version/ce.spec.ts | 2 +- .../omnichannel-chat-transfers.spec.ts | 24 ++-- ...mnichannel-contact-conflict-review.spec.ts | 14 +-- ...channel-transfer-to-another-agents.spec.ts | 4 +- .../e2e/page-objects/account-security.ts | 2 +- apps/meteor/tests/e2e/page-objects/admin.ts | 2 +- .../tests/e2e/page-objects/encrypted-room.ts | 2 +- .../tests/e2e/page-objects/fragments/e2ee.ts | 116 ------------------ .../fragments/home-omnichannel-content.ts | 6 +- .../tests/e2e/page-objects/fragments/index.ts | 3 +- .../e2e/page-objects/fragments/listbox.ts | 12 +- .../fragments/{ => modals}/apps-modal.ts | 2 - .../fragments/modals/confirm-delete-modal.ts | 18 +++ .../modals}/create-new-modal.ts | 14 +-- .../modals/disable-room-encryption-modal.ts | 23 ++++ .../{ => modals}/edit-status-modal.ts | 2 +- .../modals/enable-room-encryption-modal.ts | 23 ++++ .../modals/enter-e2ee-password-modal.ts | 32 +++++ .../{ => modals}/enter-password-modal.ts | 2 +- .../{ => modals}/file-upload-modal.ts | 0 .../page-objects/fragments/modals/index.ts | 18 +++ .../fragments/{ => modals}/modal.ts | 17 +-- .../omnichannel-close-chat-modal.ts | 2 +- .../modals/omnichannel-confirm-remove-chat.ts | 18 +++ .../omnichannel-contact-review-modal.ts | 27 ++++ .../{ => modals}/omnichannel-on-hold-modal.ts | 0 .../modals/omnichannel-transfer-chat-modal.ts | 41 +++++++ .../{ => modals}/report-message-modal.ts | 4 +- .../modals/reset-e2ee-password-modal.ts | 23 ++++ .../modals/save-e2ee-password-modal.ts | 31 +++++ .../fragments/{ => modals}/upsell-modal.ts | 0 .../e2e/page-objects/fragments/navbar.ts | 3 +- .../e2e/page-objects/fragments/toolbar.ts | 3 +- .../omnichannel-contact-center-chats.ts | 21 +--- .../omnichannel-contact-review-modal.ts | 25 ---- .../e2e/page-objects/omnichannel-info.ts | 19 ++- .../omnichannel-transfer-chat-modal.ts | 40 ------ apps/meteor/tests/e2e/team-management.spec.ts | 2 +- apps/meteor/tests/e2e/voice-calls-ce.spec.ts | 2 +- 46 files changed, 332 insertions(+), 288 deletions(-) rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/apps-modal.ts (93%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts rename apps/meteor/tests/e2e/page-objects/{ => fragments/modals}/create-new-modal.ts (90%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/disable-room-encryption-modal.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/edit-status-modal.ts (92%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/enable-room-encryption-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/enter-e2ee-password-modal.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/enter-password-modal.ts (93%) rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/file-upload-modal.ts (100%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/modal.ts (67%) rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/omnichannel-close-chat-modal.ts (94%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-confirm-remove-chat.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/omnichannel-on-hold-modal.ts (100%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/report-message-modal.ts (92%) create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/reset-e2ee-password-modal.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/modals/save-e2ee-password-modal.ts rename apps/meteor/tests/e2e/page-objects/fragments/{ => modals}/upsell-modal.ts (100%) delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-transfer-chat-modal.ts diff --git a/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx index 28af569dde703..a46b11b11920c 100644 --- a/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx +++ b/apps/meteor/client/views/omnichannel/modals/ForwardChatModal.tsx @@ -81,9 +81,9 @@ const ForwardChatModal = ({ onForward, onCancel, room, ...props }: ForwardChatMo return ( } {...props} - data-qa-id='forward-chat-modal' > diff --git a/apps/meteor/tests/e2e/apps/apps-modal.spec.ts b/apps/meteor/tests/e2e/apps/apps-modal.spec.ts index cb0718fce9c3f..62cf24c61f399 100644 --- a/apps/meteor/tests/e2e/apps/apps-modal.spec.ts +++ b/apps/meteor/tests/e2e/apps/apps-modal.spec.ts @@ -3,7 +3,7 @@ import type { Page } from '@playwright/test'; import { IS_EE } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { HomeChannel } from '../page-objects'; -import { AppsModal } from '../page-objects/fragments/apps-modal'; +import { AppsModal } from '../page-objects/fragments/modals'; import { expect, test } from '../utils/test'; test.use({ storageState: Users.user1.state }); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts index cbaa101ce0e71..e60a2777ff596 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts @@ -5,7 +5,7 @@ import { BASE_URL } from '../config/constants'; import { Users } from '../fixtures/userStates'; import { EncryptedRoomPage } from '../page-objects/encrypted-room'; import { Navbar } from '../page-objects/fragments'; -import { FileUploadModal } from '../page-objects/fragments/file-upload-modal'; +import { FileUploadModal } from '../page-objects/fragments/modals'; import { LoginPage } from '../page-objects/login'; import { createTargetGroupAndReturnFullRoom, deleteChannel, deleteRoom } from '../utils'; import { preserveSettings } from '../utils/preserveSettings'; diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts index 0d8c92b8be8a7..dd5f1e8f00e71 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-passphrase-management.spec.ts @@ -5,12 +5,8 @@ import { Users, storeState, restoreState } from '../fixtures/userStates'; import { AccountSecurity, HomeChannel } from '../page-objects'; import { setupE2EEPassword } from './setupE2EEPassword'; import { Navbar } from '../page-objects/fragments'; -import { - E2EEKeyDecodeFailureBanner, - EnterE2EEPasswordBanner, - EnterE2EEPasswordModal, - ResetE2EEPasswordModal, -} from '../page-objects/fragments/e2ee'; +import { E2EEKeyDecodeFailureBanner, EnterE2EEPasswordBanner } from '../page-objects/fragments/e2ee'; +import { EnterE2EEPasswordModal, ResetE2EEPasswordModal } from '../page-objects/fragments/modals'; import { LoginPage } from '../page-objects/login'; import { preserveSettings } from '../utils/preserveSettings'; import { test, expect } from '../utils/test'; diff --git a/apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts b/apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts index 3faf7323d4061..ebd09e5037986 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/setupE2EEPassword.ts @@ -1,6 +1,7 @@ import type { Page } from '@playwright/test'; -import { SaveE2EEPasswordBanner, SaveE2EEPasswordModal } from '../page-objects/fragments/e2ee'; +import { SaveE2EEPasswordBanner } from '../page-objects/fragments/e2ee'; +import { SaveE2EEPasswordModal } from '../page-objects/fragments/modals'; /** * Click the banner to open the dialog to save the generated password diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 31a1928c44634..840575c5df67b 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; import { AdminInfo, HomeChannel } from './page-objects'; -import { CreateNewChannelModal } from './page-objects/create-new-modal'; +import { CreateNewChannelModal } from './page-objects/fragments/modals'; import { createTargetChannel, createTargetTeam, diff --git a/apps/meteor/tests/e2e/federation/page-objects/channel.ts b/apps/meteor/tests/e2e/federation/page-objects/channel.ts index 7a21bc4a8d28a..07f05807064ab 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/channel.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/channel.ts @@ -3,8 +3,8 @@ import type { Locator, Page } from '@playwright/test'; import { FederationHomeContent } from './fragments/home-content'; import { FederationHomeFlextab } from './fragments/home-flextab'; import { FederationSidenav } from './fragments/home-sidenav'; -import { CreateNewChannelModal, CreateNewDMModal } from '../../page-objects/create-new-modal'; import { Navbar, RoomToolbar, ToastMessages } from '../../page-objects/fragments'; +import { CreateNewChannelModal, CreateNewDMModal } from '../../page-objects/fragments/modals'; export class FederationChannel { private readonly page: Page; diff --git a/apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts b/apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts index 6924d0c401b12..87c6efa02030e 100644 --- a/apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts +++ b/apps/meteor/tests/e2e/federation/tests/ce-version/ce.spec.ts @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { CreateNewChannelModal } from '../../../page-objects/create-new-modal'; +import { CreateNewChannelModal } from '../../../page-objects/fragments/modals'; import * as constants from '../../config/constants'; import { FederationChannel } from '../../page-objects/channel'; import { doLogin } from '../../utils/auth'; diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts index bda12cfc1710b..869ee0947d17e 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-chat-transfers.spec.ts @@ -141,8 +141,8 @@ test.describe('OC - Chat transfers [Monitor role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); @@ -183,8 +183,8 @@ test.describe('OC - Chat transfers [Monitor role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); @@ -227,8 +227,8 @@ test.describe('OC - Chat transfers [Monitor role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); @@ -363,8 +363,8 @@ test.describe('OC - Chat transfers [Manager role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); @@ -405,8 +405,8 @@ test.describe('OC - Chat transfers [Manager role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); @@ -449,8 +449,8 @@ test.describe('OC - Chat transfers [Manager role]', () => { }); await test.step('expect agent and department fields to be visible and enabled', async () => { - await expect(poOmnichannel.content.forwardChatModal.inputFowardUser).toBeEnabled(); - await expect(poOmnichannel.content.forwardChatModal.inputFowardDepartment).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardUser).toBeEnabled(); + await expect(poOmnichannel.content.forwardChatModal.inputForwardDepartment).toBeEnabled(); await expect(poOmnichannel.content.forwardChatModal.btnForward).toBeDisabled(); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts index d5590ffed422b..eff87a25b60b7 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-contact-conflict-review.spec.ts @@ -75,19 +75,11 @@ test.describe.serial('OC - Contact Review', () => { await customField.delete(); }); - test('OC - Contact Review - Update custom field conflicting', async ({ page }) => { + test('OC - Contact Review - Update custom field conflicting', async () => { await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); await poHomeChannel.roomToolbar.openContactInfo(); + await poHomeChannel.contacts.contactInfo.solveConflict(customFieldName, 'custom-field-value-2'); - await poHomeChannel.content.contactReviewModal.btnSeeConflicts.click(); - - await poHomeChannel.content.contactReviewModal.getFieldByName(customFieldName).click(); - await poHomeChannel.content.contactReviewModal.findOption('custom-field-value-2').click(); - const responseListener = page.waitForResponse('**/api/v1/omnichannel/contacts.conflicts'); - await poHomeChannel.content.contactReviewModal.btnSave.click(); - const response = await responseListener; - await expect(response.status()).toBe(200); - - await expect(poHomeChannel.content.contactReviewModal.btnSeeConflicts).not.toBeVisible(); + await expect(poHomeChannel.contacts.contactInfo.btnSeeConflicts).not.toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts index 13938fd830527..bcd0ff9afa2cc 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts @@ -73,8 +73,8 @@ test.describe('OC - Chat transfers [Agent role]', () => { await agentB.poHomeOmnichannel.navbar.changeUserStatus('offline'); await agentA.poHomeOmnichannel.quickActionsRoomToolbar.forwardChat(); - await agentA.poHomeOmnichannel.content.forwardChatModal.inputFowardUser.click(); - await agentA.poHomeOmnichannel.content.forwardChatModal.inputFowardUser.type('user2'); + await agentA.poHomeOmnichannel.content.forwardChatModal.inputForwardUser.click(); + await agentA.poHomeOmnichannel.content.forwardChatModal.inputForwardUser.type('user2'); await expect(agentA.page.locator('text=Empty')).toBeVisible(); await agentA.page.goto('/'); diff --git a/apps/meteor/tests/e2e/page-objects/account-security.ts b/apps/meteor/tests/e2e/page-objects/account-security.ts index dba166964e60e..dfeb2ff8ec5cd 100644 --- a/apps/meteor/tests/e2e/page-objects/account-security.ts +++ b/apps/meteor/tests/e2e/page-objects/account-security.ts @@ -1,7 +1,7 @@ import type { Locator, Page } from '@playwright/test'; import { Account } from './account'; -import { EnterPasswordModal } from './fragments/enter-password-modal'; +import { EnterPasswordModal } from './fragments/modals'; export class AccountSecurity extends Account { private readonly enterPasswordModal: EnterPasswordModal; diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index 688ec8a1728f3..e6ad6679f5189 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -1,7 +1,7 @@ import type { Locator, Page } from '@playwright/test'; import { AdminSidebar, ToastMessages } from './fragments'; -import { ConfirmDeleteModal } from './fragments/modal'; +import { ConfirmDeleteModal } from './fragments/modals'; export enum AdminSectionsHref { Workspace = '/admin/info', diff --git a/apps/meteor/tests/e2e/page-objects/encrypted-room.ts b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts index efbb84b623116..4b8a4e98c1e59 100644 --- a/apps/meteor/tests/e2e/page-objects/encrypted-room.ts +++ b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts @@ -1,6 +1,6 @@ import { HomeContent, HomeFlextab } from './fragments'; -import { DisableRoomEncryptionModal, EnableRoomEncryptionModal } from './fragments/e2ee'; import { Message } from './fragments/message'; +import { DisableRoomEncryptionModal, EnableRoomEncryptionModal } from './fragments/modals'; export class EncryptedRoomPage extends HomeContent { get encryptedTitle() { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts index 151915016bbc2..657e2a643e3c0 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts @@ -1,9 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { Modal } from './modal'; -import { ToastMessages } from './toast-messages'; import { expect } from '../../utils/test'; -import { LoginPage } from '../login'; abstract class E2EEBanner { constructor(protected root: Locator) {} @@ -39,116 +36,3 @@ export class E2EEKeyDecodeFailureBanner extends E2EEBanner { await expect(this.root).not.toBeVisible(); } } - -export class SaveE2EEPasswordModal extends Modal { - private readonly toastMessages: ToastMessages; - - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Save your new E2EE password' })); - this.toastMessages = new ToastMessages(page); - } - - private get password() { - return this.root.getByLabel('Your E2EE password is:').getByRole('code'); - } - - private get savedPasswordButton() { - return this.root.getByRole('button', { name: 'I saved my password' }); - } - - async getPassword() { - return (await this.password.textContent()) ?? ''; - } - - async confirm() { - await this.savedPasswordButton.click(); - await this.waitForDismissal(); - await this.toastMessages.dismissToast('success'); - } -} - -export class EnterE2EEPasswordModal extends Modal { - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Enter E2EE password' })); - } - - private get passwordInput() { - return this.root.getByPlaceholder('Please enter your E2EE password'); - } - - private get forgotPasswordLink() { - return this.root.getByRole('link', { name: 'Forgot E2EE password?' }); - } - - private get enterE2EEPasswordButton() { - return this.root.getByRole('button', { name: 'Enable encryption' }); - } - - async enterPassword(password: string) { - await this.passwordInput.fill(password); - await this.enterE2EEPasswordButton.click(); - await this.waitForDismissal(); - } - - async forgotPassword() { - await this.forgotPasswordLink.click(); - await this.waitForDismissal(); - } -} - -export class ResetE2EEPasswordModal extends Modal { - private readonly login: LoginPage; - - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Reset E2EE password' })); - this.login = new LoginPage(page); - } - - private get resetE2EEPasswordButton() { - return this.root.getByRole('button', { name: 'Reset E2EE password' }); - } - - async confirmReset() { - await this.resetE2EEPasswordButton.click(); - await this.waitForDismissal(); - await this.login.waitForIt(); - } -} - -export class EnableRoomEncryptionModal extends Modal { - private readonly toastMessages: ToastMessages; - - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Enable encryption' })); - this.toastMessages = new ToastMessages(page); - } - - private get enableButton() { - return this.root.getByRole('button', { name: 'Enable encryption' }); - } - - async enable() { - await this.enableButton.click(); - await this.waitForDismissal(); - await this.toastMessages.dismissToast('success'); - } -} - -export class DisableRoomEncryptionModal extends Modal { - private readonly toastMessages: ToastMessages; - - constructor(page: Page) { - super(page.getByRole('dialog', { name: 'Disable encryption' })); - this.toastMessages = new ToastMessages(page); - } - - private get disableButton() { - return this.root.getByRole('button', { name: 'Disable encryption' }); - } - - async disable() { - await this.disableButton.click(); - await this.waitForDismissal(); - await this.toastMessages.dismissToast('success'); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index 4bcbe6e999dfb..b15aebfaa611b 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -1,18 +1,14 @@ import type { Locator, Page } from '@playwright/test'; -import { OmnichannelTransferChatModal } from '../omnichannel-transfer-chat-modal'; import { HomeContent } from './home-content'; -import { OmnichannelContactReviewModal } from '../omnichannel-contact-review-modal'; +import { OmnichannelTransferChatModal } from './modals'; export class HomeOmnichannelContent extends HomeContent { readonly forwardChatModal: OmnichannelTransferChatModal; - readonly contactReviewModal: OmnichannelContactReviewModal; - constructor(page: Page) { super(page); this.forwardChatModal = new OmnichannelTransferChatModal(page); - this.contactReviewModal = new OmnichannelContactReviewModal(page); } get btnReturnToQueue(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index a46cbe188f3bb..18053e6b80bde 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -5,11 +5,10 @@ export * from './home-omnichannel-content'; export * from './home-flextab'; export * from './home-sidenav'; export * from './omnichannel-sidenav'; -export * from './omnichannel-close-chat-modal'; export * from './navbar'; export * from './sidebar'; export * from './sidepanel'; -export * from './report-message-modal'; +export * from './modals'; export * from './toast-messages'; export * from './export-messages-tab'; export * from './menu'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts b/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts index 52aa10bab4399..055c7ccb56b84 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/listbox.ts @@ -1,9 +1,13 @@ -import type { Locator, Page } from 'playwright-core'; +import type { Locator } from 'playwright-core'; export class Listbox { - readonly root: Locator; + constructor(private root: Locator) {} - constructor(page: Page) { - this.root = page.getByRole('listbox'); + async selectOption(name: string) { + return this.root.getByRole('option', { name }).click(); + } + + public getOption(name: string): Locator { + return this.root.getByRole('option', { name }); } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/apps-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/apps-modal.ts similarity index 93% rename from apps/meteor/tests/e2e/page-objects/fragments/apps-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/apps-modal.ts index c006a9714c8ca..0f5ac018c357d 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/apps-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/apps-modal.ts @@ -3,8 +3,6 @@ import type { Locator, Page } from 'playwright-core'; import { Modal } from './modal'; export class AppsModal extends Modal { - protected override readonly page: Page; - constructor(page: Page) { super(page.getByRole('dialog', { name: 'Modal Example' })); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts new file mode 100644 index 0000000000000..66826b0489d40 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/confirm-delete-modal.ts @@ -0,0 +1,18 @@ +import type { Locator } from 'playwright-core'; + +import { Modal } from './modal'; + +export class ConfirmDeleteModal extends Modal { + constructor(root: Locator) { + super(root); + } + + private btnDelete() { + return this.root.getByRole('button', { name: 'Delete' }); + } + + async confirmDelete() { + await this.btnDelete().click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/create-new-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts similarity index 90% rename from apps/meteor/tests/e2e/page-objects/create-new-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts index 759676325e6b7..5d1f33efd0b12 100644 --- a/apps/meteor/tests/e2e/page-objects/create-new-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/create-new-modal.ts @@ -1,14 +1,14 @@ import type { Locator, Page } from '@playwright/test'; -import { Listbox } from './fragments/listbox'; -import { Modal } from './fragments/modal'; +import { Listbox } from '../listbox'; +import { Modal } from './modal'; -export class CreateNewModal extends Modal { - readonly listbox: Locator; +export abstract class CreateNewModal extends Modal { + readonly listbox: Listbox; constructor(root: Locator, page: Page) { super(root, page); - this.listbox = new Listbox(page).root; + this.listbox = new Listbox(page.getByRole('listbox')); } get inputName(): Locator { @@ -38,7 +38,7 @@ export class CreateNewModal extends Modal { async addMember(memberName: string): Promise { await this.root.getByRole('textbox', { name: 'Add people' }).click(); await this.root.getByRole('textbox', { name: 'Add people' }).fill(memberName, { force: true }); - await this.listbox.getByRole('option', { name: memberName }).click(); + await this.listbox.selectOption(memberName); await this.root.getByRole('textbox', { name: 'Add people' }).click(); } } @@ -106,7 +106,7 @@ export class CreateNewDiscussionModal extends CreateNewModal { } getParentRoomListItem(name: string): Locator { - return this.listbox.getByRole('option', { name }); + return this.listbox.getOption(name); } get inputMessage(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/disable-room-encryption-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/disable-room-encryption-modal.ts new file mode 100644 index 0000000000000..35690e7c7b080 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/disable-room-encryption-modal.ts @@ -0,0 +1,23 @@ +import type { Page } from 'playwright-core'; + +import { Modal } from './modal'; +import { ToastMessages } from '../toast-messages'; + +export class DisableRoomEncryptionModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Disable encryption' })); + this.toastMessages = new ToastMessages(page); + } + + private get disableButton() { + return this.root.getByRole('button', { name: 'Disable encryption' }); + } + + async disable() { + await this.disableButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-status-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts similarity index 92% rename from apps/meteor/tests/e2e/page-objects/fragments/edit-status-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts index 46633ec9a907b..71ca5ceb99f42 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/edit-status-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/edit-status-modal.ts @@ -1,7 +1,7 @@ import type { Page } from 'playwright-core'; import { Modal } from './modal'; -import { ToastMessages } from './toast-messages'; +import { ToastMessages } from '../toast-messages'; export class EditStatusModal extends Modal { readonly toastMessages: ToastMessages; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/enable-room-encryption-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/enable-room-encryption-modal.ts new file mode 100644 index 0000000000000..a8205ec1caeaa --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/enable-room-encryption-modal.ts @@ -0,0 +1,23 @@ +import type { Page } from 'playwright-core'; + +import { Modal } from './modal'; +import { ToastMessages } from '../toast-messages'; + +export class EnableRoomEncryptionModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Enable encryption' })); + this.toastMessages = new ToastMessages(page); + } + + private get enableButton() { + return this.root.getByRole('button', { name: 'Enable encryption' }); + } + + async enable() { + await this.enableButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/enter-e2ee-password-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/enter-e2ee-password-modal.ts new file mode 100644 index 0000000000000..cb5044a21f607 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/enter-e2ee-password-modal.ts @@ -0,0 +1,32 @@ +import type { Page } from '@playwright/test'; + +import { Modal } from './modal'; + +export class EnterE2EEPasswordModal extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Enter E2EE password' })); + } + + private get passwordInput() { + return this.root.getByPlaceholder('Please enter your E2EE password'); + } + + private get forgotPasswordLink() { + return this.root.getByRole('link', { name: 'Forgot E2EE password?' }); + } + + private get enterE2EEPasswordButton() { + return this.root.getByRole('button', { name: 'Enable encryption' }); + } + + async enterPassword(password: string) { + await this.passwordInput.fill(password); + await this.enterE2EEPasswordButton.click(); + await this.waitForDismissal(); + } + + async forgotPassword() { + await this.forgotPasswordLink.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/enter-password-modal.ts similarity index 93% rename from apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/enter-password-modal.ts index 95c022c88de18..c76852ec11960 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/enter-password-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/enter-password-modal.ts @@ -1,7 +1,7 @@ import type { Page } from 'playwright-core'; import { Modal } from './modal'; -import { ToastMessages } from './toast-messages'; +import { ToastMessages } from '../toast-messages'; export class EnterPasswordModal extends Modal { readonly toastMessages: ToastMessages; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts similarity index 100% rename from apps/meteor/tests/e2e/page-objects/fragments/file-upload-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/file-upload-modal.ts diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts new file mode 100644 index 0000000000000..462b2836f5ec2 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts @@ -0,0 +1,18 @@ +export * from './apps-modal'; +export * from './confirm-delete-modal'; +export * from './create-new-modal'; +export * from './disable-room-encryption-modal'; +export * from './edit-status-modal'; +export * from './enable-room-encryption-modal'; +export * from './enter-e2ee-password-modal'; +export * from './enter-password-modal'; +export * from './file-upload-modal'; +export * from './omnichannel-close-chat-modal'; +export * from './omnichannel-on-hold-modal'; +export * from './omnichannel-transfer-chat-modal'; +export * from './omnichannel-confirm-remove-chat'; +export * from './omnichannel-contact-review-modal'; +export * from './report-message-modal'; +export * from './reset-e2ee-password-modal'; +export * from './save-e2ee-password-modal'; +export * from './upsell-modal'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/modal.ts similarity index 67% rename from apps/meteor/tests/e2e/page-objects/fragments/modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/modal.ts index 3d61a366195f9..f6c9d2036b541 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/modal.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { expect } from '../../utils/test'; +import { expect } from '../../../utils/test'; export abstract class Modal { constructor( @@ -37,18 +37,3 @@ export abstract class Modal { await this.waitForDismissal(); } } - -export class ConfirmDeleteModal extends Modal { - constructor(root: Locator) { - super(root); - } - - private btnDelete() { - return this.root.getByRole('button', { name: 'Delete' }); - } - - async confirmDelete() { - await this.btnDelete().click(); - await this.waitForDismissal(); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-close-chat-modal.ts similarity index 94% rename from apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-close-chat-modal.ts index 4833e16075e14..e17df57aec773 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-close-chat-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-close-chat-modal.ts @@ -1,7 +1,7 @@ import type { Locator, Page } from '@playwright/test'; import { Modal } from './modal'; -import { ToastMessages } from './toast-messages'; +import { ToastMessages } from '../toast-messages'; export class OmnichannelCloseChatModal extends Modal { private readonly toastMessages: ToastMessages; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-confirm-remove-chat.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-confirm-remove-chat.ts new file mode 100644 index 0000000000000..42528f748cef0 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-confirm-remove-chat.ts @@ -0,0 +1,18 @@ +import type { Locator, Page } from 'playwright-core'; + +import { Modal } from './modal'; + +export class OmnichannelConfirmRemoveChat extends Modal { + constructor(page: Page) { + super(page.getByRole('dialog')); + } + + private get btnConfirmRemove(): Locator { + return this.root.getByRole('button', { name: 'Delete' }); + } + + async confirm() { + await this.btnConfirmRemove.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts new file mode 100644 index 0000000000000..b4314202283a9 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-contact-review-modal.ts @@ -0,0 +1,27 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Modal } from './modal'; +import { expect } from '../../../utils/test'; +import { Listbox } from '../listbox'; + +export class OmnichannelContactReviewModal extends Modal { + readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Review contact' }), page); + this.listbox = new Listbox(page.getByRole('listbox')); + } + + private getFieldByName(name: string): Locator { + return this.root.getByLabel(name, { exact: true }); + } + + async solveConfirmation(field: string, value: string) { + await this.getFieldByName(field).click(); + await this.listbox.selectOption(value); + const responsePromise = this.page?.waitForResponse('**/api/v1/omnichannel/contacts.conflicts'); + await this.save(); + const responseListener = await responsePromise; + expect(responseListener?.status()).toBe(200); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-on-hold-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-on-hold-modal.ts similarity index 100% rename from apps/meteor/tests/e2e/page-objects/fragments/omnichannel-on-hold-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-on-hold-modal.ts diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts new file mode 100644 index 0000000000000..12dcfa148f73e --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/omnichannel-transfer-chat-modal.ts @@ -0,0 +1,41 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Modal } from './modal'; +import { Listbox } from '../listbox'; + +export class OmnichannelTransferChatModal extends Modal { + private readonly listbox: Listbox; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Forward chat' })); + this.listbox = new Listbox(page.getByRole('listbox')); + } + + get inputComment(): Locator { + return this.root.locator('textarea[name="comment"]'); + } + + get inputForwardDepartment(): Locator { + return this.root.locator('[data-qa-id="forward-to-department"] input'); + } + + get inputForwardUser(): Locator { + return this.root.locator('[data-qa="autocomplete-agent"] input'); + } + + get btnForward(): Locator { + return this.root.locator('role=button[name="Forward"]'); + } + + async selectDepartment(name: string) { + await this.inputForwardDepartment.click(); + await this.inputForwardDepartment.fill(name); + await this.listbox.selectOption(name); + } + + async selectUser(name: string, id?: string) { + await this.inputForwardUser.click(); + await this.inputForwardUser.fill(name); + await this.listbox.selectOption(id || name); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/report-message-modal.ts similarity index 92% rename from apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/report-message-modal.ts index 2ac3d46f35f9a..40d37c4a98156 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/report-message-modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/report-message-modal.ts @@ -1,8 +1,8 @@ import type { Locator, Page } from '@playwright/test'; import { Modal } from './modal'; -import { ToastMessages } from './toast-messages'; -import { expect } from '../../utils/test'; +import { expect } from '../../../utils/test'; +import { ToastMessages } from '../toast-messages'; export class ReportMessageModal extends Modal { readonly toastMessage: ToastMessages; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/reset-e2ee-password-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/reset-e2ee-password-modal.ts new file mode 100644 index 0000000000000..ecc6d346a06a5 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/reset-e2ee-password-modal.ts @@ -0,0 +1,23 @@ +import type { Page } from 'playwright-core'; + +import { Modal } from './modal'; +import { LoginPage } from '../../login'; + +export class ResetE2EEPasswordModal extends Modal { + private readonly login: LoginPage; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Reset E2EE password' })); + this.login = new LoginPage(page); + } + + private get resetE2EEPasswordButton() { + return this.root.getByRole('button', { name: 'Reset E2EE password' }); + } + + async confirmReset() { + await this.resetE2EEPasswordButton.click(); + await this.waitForDismissal(); + await this.login.waitForIt(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modals/save-e2ee-password-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/save-e2ee-password-modal.ts new file mode 100644 index 0000000000000..2c849af224683 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/modals/save-e2ee-password-modal.ts @@ -0,0 +1,31 @@ +import type { Page } from 'playwright-core'; + +import { Modal } from './modal'; +import { ToastMessages } from '../toast-messages'; + +export class SaveE2EEPasswordModal extends Modal { + private readonly toastMessages: ToastMessages; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Save your new E2EE password' })); + this.toastMessages = new ToastMessages(page); + } + + private get password() { + return this.root.getByLabel('Your E2EE password is:').getByRole('code'); + } + + private get savedPasswordButton() { + return this.root.getByRole('button', { name: 'I saved my password' }); + } + + async getPassword() { + return (await this.password.textContent()) ?? ''; + } + + async confirm() { + await this.savedPasswordButton.click(); + await this.waitForDismissal(); + await this.toastMessages.dismissToast('success'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/upsell-modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts similarity index 100% rename from apps/meteor/tests/e2e/page-objects/fragments/upsell-modal.ts rename to apps/meteor/tests/e2e/page-objects/fragments/modals/upsell-modal.ts diff --git a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts index bf10c74603f72..1365b8a13ee6c 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/navbar.ts @@ -1,8 +1,7 @@ import type { Locator, Page } from '@playwright/test'; +import { EditStatusModal, CreateNewChannelModal, CreateNewDiscussionModal, CreateNewDMModal, CreateNewTeamModal } from './modals'; import { expect } from '../../utils/test'; -import { CreateNewChannelModal, CreateNewDiscussionModal, CreateNewDMModal, CreateNewTeamModal } from '../create-new-modal'; -import { EditStatusModal } from './edit-status-modal'; export class Navbar { private readonly modals: { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts b/apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts index 0aac785130046..104d39fc05a13 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts @@ -1,7 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { OmnichannelCloseChatModal } from './omnichannel-close-chat-modal'; -import { OmnichannelOnHoldModal } from './omnichannel-on-hold-modal'; +import { OmnichannelCloseChatModal, OmnichannelOnHoldModal } from './modals'; export abstract class Toolbar { constructor(protected root: Locator) {} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts index 9ad2fbdad7260..339a3b96621bb 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-center-chats.ts @@ -2,25 +2,10 @@ import type { Locator, Page } from '@playwright/test'; import { HomeOmnichannelContent } from './fragments'; import { FlexTab } from './fragments/flextab'; -import { Modal } from './fragments/modal'; +import { OmnichannelConfirmRemoveChat } from './fragments/modals'; import { OmnichannelAdministration } from './omnichannel-administration'; import { OmnichannelChatsFilters } from './omnichannel-contact-center-chats-filters'; -class ConfirmRemoveChat extends Modal { - constructor(page: Page) { - super(page.getByRole('dialog')); - } - - private get btnConfirmRemove(): Locator { - return this.root.getByRole('button', { name: 'Delete' }); - } - - async confirm() { - await this.btnConfirmRemove.click(); - await this.waitForDismissal(); - } -} - class ConversationFlexTab extends FlexTab { constructor(page: Page) { super(page.getByRole('dialog', { name: 'Conversation' })); @@ -37,7 +22,7 @@ class ConversationFlexTab extends FlexTab { } export class OmnichannelChats extends OmnichannelAdministration { - private readonly confirmRemoveChatModal: ConfirmRemoveChat; + private readonly confirmRemoveChatModal: OmnichannelConfirmRemoveChat; private readonly conversation: ConversationFlexTab; @@ -48,7 +33,7 @@ export class OmnichannelChats extends OmnichannelAdministration { constructor(page: Page) { super(page); this.filters = new OmnichannelChatsFilters(page); - this.confirmRemoveChatModal = new ConfirmRemoveChat(page); + this.confirmRemoveChatModal = new OmnichannelConfirmRemoveChat(page); this.content = new HomeOmnichannelContent(page); this.conversation = new ConversationFlexTab(page); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts deleted file mode 100644 index 520faa2891ada..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-contact-review-modal.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelContactReviewModal { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get btnSeeConflicts(): Locator { - return this.page.getByRole('button', { name: 'See conflicts', exact: true }); - } - - get btnSave(): Locator { - return this.page.getByRole('button', { name: 'Save', exact: true }); - } - - getFieldByName(name: string): Locator { - return this.page.getByLabel(name, { exact: true }); - } - - findOption(name: string): Locator { - return this.page.getByRole('option', { name, exact: true }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts index 665d34418ae58..c6b5dc3e25e81 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts @@ -1,8 +1,16 @@ -import type { Locator } from '@playwright/test'; +import type { Locator, Page } from '@playwright/test'; +import { OmnichannelContactReviewModal } from './fragments'; import { OmnichannelManageContact } from './omnichannel-manage-contact'; export class OmnichannelContactInfo extends OmnichannelManageContact { + readonly contactReviewModal: OmnichannelContactReviewModal; + + constructor(page: Page) { + super(page); + this.contactReviewModal = new OmnichannelContactReviewModal(page); + } + get dialogContactInfo(): Locator { return this.page.getByRole('dialog', { name: 'Contact' }); } @@ -26,4 +34,13 @@ export class OmnichannelContactInfo extends OmnichannelManageContact { get btnOpenChat(): Locator { return this.dialogContactInfo.getByRole('button', { name: 'Open chat' }); } + + get btnSeeConflicts(): Locator { + return this.dialogContactInfo.getByRole('button', { name: 'See conflicts' }); + } + + async solveConflict(field: string, value: string) { + await this.btnSeeConflicts.click(); + await this.contactReviewModal.solveConfirmation(field, value); + } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-transfer-chat-modal.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-transfer-chat-modal.ts deleted file mode 100644 index f2e70a496ca18..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-transfer-chat-modal.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class OmnichannelTransferChatModal { - private readonly page: Page; - - private readonly dialog: Locator; - - constructor(page: Page) { - this.page = page; - this.dialog = page.locator('[data-qa-id="forward-chat-modal"]'); - } - - get inputComment(): Locator { - return this.dialog.locator('textarea[name="comment"]'); - } - - get inputFowardDepartment(): Locator { - return this.dialog.locator('[data-qa-id="forward-to-department"] input'); - } - - get inputFowardUser(): Locator { - return this.dialog.locator('[data-qa="autocomplete-agent"] input'); - } - - get btnForward(): Locator { - return this.dialog.locator('role=button[name="Forward"]'); - } - - async selectDepartment(name: string) { - await this.inputFowardDepartment.click(); - await this.inputFowardDepartment.fill(name); - await this.page.locator(`li[role="option"]`, { hasText: name }).click(); - } - - async selectUser(name: string, id?: string) { - await this.inputFowardUser.click(); - await this.inputFowardUser.fill(name); - await this.page.locator(`li[role="option"][value="${id || name}"]`).click(); - } -} diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index a8146f82bb9b3..81f76ef60e628 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; import { HomeTeam } from './page-objects'; -import { CreateNewTeamModal, CreateNewChannelModal } from './page-objects/create-new-modal'; +import { CreateNewTeamModal, CreateNewChannelModal } from './page-objects/fragments/modals'; import { createTargetChannel } from './utils'; import { expect, test } from './utils/test'; diff --git a/apps/meteor/tests/e2e/voice-calls-ce.spec.ts b/apps/meteor/tests/e2e/voice-calls-ce.spec.ts index 1b5bba7dd9c3f..6c58ab16d5e32 100644 --- a/apps/meteor/tests/e2e/voice-calls-ce.spec.ts +++ b/apps/meteor/tests/e2e/voice-calls-ce.spec.ts @@ -1,7 +1,7 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { HomeChannel } from './page-objects'; -import { VoiceCallsUpsellModal } from './page-objects/fragments/upsell-modal'; +import { VoiceCallsUpsellModal } from './page-objects/fragments/modals'; import { expect, test } from './utils/test'; test.use({ storageState: Users.user1.state }); From f29a04e531267d888c529296c4830133622ab046 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 25 Dec 2025 11:35:55 -0600 Subject: [PATCH 011/104] chore: Logging (#37956) --- .../app/api/server/helpers/parseJsonQuery.ts | 24 ++++++++++---- apps/meteor/app/cors/server/cors.ts | 6 +--- apps/meteor/app/crowd/server/crowd.ts | 32 +++++++++---------- .../app/importer-csv/server/CsvImporter.ts | 12 +++---- .../server/PendingAvatarImporter.ts | 6 ++-- .../importer-slack/server/SlackImporter.ts | 19 ++++++----- .../app/importer/server/classes/Importer.ts | 14 +++++--- .../classes/converters/RecordConverter.ts | 6 +++- .../classes/converters/RoomConverter.ts | 8 +++-- .../meteor/app/integrations/server/api/api.ts | 2 +- 10 files changed, 75 insertions(+), 54 deletions(-) diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index bd7fc4f673dc0..f21e7b28efe07 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -43,8 +43,12 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise } }); } catch (e) { - logger.warn(`Invalid sort parameter provided "${params.sort}":`, e); - throw new Meteor.Error('error-invalid-sort', `Invalid sort parameter provided: "${params.sort}"`, { + logger.warn({ + msg: 'Invalid sort parameter provided', + sort: params.sort, + err: e, + }); + throw new Meteor.Error('error-invalid-sort', `Invalid sort parameter provided: \"${params.sort}\"`, { helperMethod: 'parseJsonQuery', }); } @@ -67,8 +71,12 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise } }); } catch (e) { - logger.warn(`Invalid fields parameter provided "${params.fields}":`, e); - throw new Meteor.Error('error-invalid-fields', `Invalid fields parameter provided: "${params.fields}"`, { + logger.warn({ + msg: 'Invalid fields parameter provided', + fields: params.fields, + err: e, + }); + throw new Meteor.Error('error-invalid-fields', `Invalid fields parameter provided: \"${params.fields}\"`, { helperMethod: 'parseJsonQuery', }); } @@ -111,8 +119,12 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise query = ejson.parse(params.query); query = clean(query, pathAllowConf.def); } catch (e) { - logger.warn(`Invalid query parameter provided "${params.query}":`, e); - throw new Meteor.Error('error-invalid-query', `Invalid query parameter provided: "${params.query}"`, { + logger.warn({ + msg: 'Invalid query parameter provided', + query: params.query, + err: e, + }); + throw new Meteor.Error('error-invalid-query', `Invalid query parameter provided: \"${params.query}\"`, { helperMethod: 'parseJsonQuery', }); } diff --git a/apps/meteor/app/cors/server/cors.ts b/apps/meteor/app/cors/server/cors.ts index 12ec52d0ab051..ef70370770741 100644 --- a/apps/meteor/app/cors/server/cors.ts +++ b/apps/meteor/app/cors/server/cors.ts @@ -169,11 +169,7 @@ WebApp.httpServer.addListener('request', (req, res, ...args) => { // @ts-expect-error - `pair` is valid, but doesnt exists on types const isSsl = req.connection.pair || (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'].indexOf('https') !== -1); - logger.debug('req.url', req.url); - logger.debug('remoteAddress', remoteAddress); - logger.debug('isLocal', isLocal); - logger.debug('isSsl', isSsl); - logger.debug('req.headers', req.headers); + logger.debug({ msg: 'CORS request info', url: req.url, remoteAddress, isLocal, isSsl, headers: req.headers }); if (!isLocal && !isSsl) { let host = req.headers.host || url.parse(Meteor.absoluteUrl()).hostname || ''; diff --git a/apps/meteor/app/crowd/server/crowd.ts b/apps/meteor/app/crowd/server/crowd.ts index 3219a851c8c42..ac1467dedbe00 100644 --- a/apps/meteor/app/crowd/server/crowd.ts +++ b/apps/meteor/app/crowd/server/crowd.ts @@ -25,7 +25,7 @@ function fallbackDefaultAccountSystem(bind: typeof Accounts, username: string | } } - logger.info('Fallback to default account system', username); + logger.info({ msg: 'Fallback to default account system', username }); const loginRequest = { user: username, @@ -125,7 +125,7 @@ export class CROWD { if (user) { crowdUsername = user.crowd_username; } else { - logger.debug('Could not find a user by email', username); + logger.debug({ msg: 'Could not find a user by email', username }); } } @@ -143,7 +143,7 @@ export class CROWD { if (user) { crowdUsername = user.crowd_username; } else { - logger.debug('Could not find a user with by crowd_username', username); + logger.debug({ msg: 'Could not find a user with by crowd_username', username }); } } @@ -157,7 +157,7 @@ export class CROWD { if (!user && crowdUsername) { logger.debug('New user. User is not synced yet.'); } - logger.debug('Going to crowd:', crowdUsername); + logger.debug({ msg: 'Going to crowd', crowdUsername }); return new Promise((resolve, reject) => this.crowdClient.user.authenticate(crowdUsername, password, async (err: any, res: Record) => { @@ -241,9 +241,9 @@ export class CROWD { for await (const user of users) { let crowdUsername = user.hasOwnProperty('crowd_username') ? user.crowd_username : user.username; - logger.info('Syncing user', crowdUsername); + logger.info({ msg: 'Syncing user', crowdUsername }); if (!crowdUsername) { - logger.warn('User has no crowd_username', user.username); + logger.warn({ msg: 'User has no crowd_username', username: user.username }); continue; } @@ -252,28 +252,28 @@ export class CROWD { try { crowdUser = await this.fetchCrowdUser(crowdUsername); } catch (err) { - logger.debug({ err }); + logger.debug({ msg: 'Error while syncing user from CROWD', err }); logger.error({ msg: 'Could not sync user with username', crowd_username: crowdUsername }); const email = user.emails?.[0].address; - logger.info('Attempting to find for user by email', email); + logger.info({ msg: 'Attempting to find user by email', email }); const response = await this.searchForCrowdUserByMail(email); if (!response || response.users.length === 0) { - logger.warn('Could not find user in CROWD with username or email:', crowdUsername, email); + logger.warn({ msg: 'Could not find user in CROWD with username or email', crowd_username: crowdUsername, email }); if (settings.get('CROWD_Remove_Orphaned_Users') === true) { - logger.info('Removing user:', crowdUsername); + logger.info({ msg: 'Removing user', crowd_username: crowdUsername }); setImmediate(async () => { await deleteUser(user._id); - logger.info('User removed:', crowdUsername); + logger.info({ msg: 'User removed', crowd_username: crowdUsername }); }); } return; } crowdUsername = response.users[0].name; - logger.info('User found by email. Syncing user', crowdUsername); + logger.info({ msg: 'User found by email. Syncing user', crowd_username: crowdUsername }); if (!crowdUsername) { - logger.warn('User has no crowd_username', user.username); + logger.warn({ msg: 'User has no crowd_username', username: user.username }); continue; } @@ -368,7 +368,7 @@ Accounts.registerLoginHandler('crowd', async function (this: typeof Accounts, lo return undefined; } - logger.info('Init CROWD login', loginRequest.username); + logger.info({ msg: 'Init CROWD login', username: loginRequest.username }); if (settings.get('CROWD_Enable') !== true) { return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.crowdPassword); @@ -379,12 +379,12 @@ Accounts.registerLoginHandler('crowd', async function (this: typeof Accounts, lo const user = await crowd.authenticate(loginRequest.username, loginRequest.crowdPassword); if (user && user.crowd === false) { - logger.debug(`User ${loginRequest.username} is not a valid crowd user, falling back`); + logger.debug({ msg: 'User is not a valid crowd user, falling back', username: loginRequest.username }); return fallbackDefaultAccountSystem(this, loginRequest.username, loginRequest.crowdPassword); } if (!user) { - logger.debug(`User ${loginRequest.username} is not allowed to access Rocket.Chat`); + logger.debug({ msg: 'User is not allowed to access Rocket.Chat', username: loginRequest.username }); return new Meteor.Error('not-authorized', 'User is not authorized by crowd'); } diff --git a/apps/meteor/app/importer-csv/server/CsvImporter.ts b/apps/meteor/app/importer-csv/server/CsvImporter.ts index cab23a46da62a..db36210271bb4 100644 --- a/apps/meteor/app/importer-csv/server/CsvImporter.ts +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -40,7 +40,7 @@ export class CsvImporter extends Importer { oldRate = rate; } } catch (e) { - this.logger.error(e); + this.logger.error({ msg: 'Error while increasing CSV import progress', err: e }); } }; @@ -66,18 +66,18 @@ export class CsvImporter extends Importer { }; for await (const entry of zip.getEntries()) { - this.logger.debug(`Entry: ${entry.entryName}`); + this.logger.debug({ msg: 'Entry', entryName: entry.entryName }); // Ignore anything that has `__MACOSX` in it's name, as sadly these things seem to mess everything up if (entry.entryName.indexOf('__MACOSX') > -1) { - this.logger.debug(`Ignoring the file: ${entry.entryName}`); + this.logger.debug({ msg: 'Ignoring the file', entryName: entry.entryName }); increaseProgressCount(); continue; } // Directories are ignored, since they are "virtual" in a zip file if (entry.isDirectory) { - this.logger.debug(`Ignoring the directory entry: ${entry.entryName}`); + this.logger.debug({ msg: 'Ignoring the directory entry', entryName: entry.entryName }); increaseProgressCount(); continue; } @@ -168,7 +168,7 @@ export class CsvImporter extends Importer { try { msgs = this.csvParser(entry.getData().toString()); } catch (e) { - this.logger.warn(`The file ${entry.entryName} contains invalid syntax`, e); + this.logger.warn({ msg: 'The file contains invalid syntax', entryName: entry.entryName, err: e }); increaseProgressCount(); continue; } @@ -277,7 +277,7 @@ export class CsvImporter extends Importer { // Ensure we have at least a single record of any kind if (usersCount === 0 && channelsCount === 0 && messagesCount === 0 && contactsCount === 0) { - this.logger.error('No valid record found in the import file.'); + this.logger.error({ msg: 'No valid record found in the import file.' }); await super.updateProgress(ProgressStep.ERROR); } diff --git a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts index 746bb41681336..6c03aba53e93e 100644 --- a/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts +++ b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts @@ -7,7 +7,7 @@ import { setAvatarFromServiceWithValidation } from '../../lib/server/functions/s export class PendingAvatarImporter extends Importer { async prepareFileCount() { - this.logger.debug('start preparing import operation'); + this.logger.debug({ msg: 'start preparing import operation' }); await super.updateProgress(ProgressStep.PREPARING_STARTED); const fileCount = await Users.countAllUsersWithPendingAvatar(); @@ -49,13 +49,13 @@ export class PendingAvatarImporter extends Importer { await setAvatarFromServiceWithValidation(_id, url, undefined, 'url'); await Users.updateOne({ _id }, { $unset: { _pendingAvatarUrl: '' } }); } catch (error) { - this.logger.warn(`Failed to set ${name}'s avatar from url ${url}`); + this.logger.warn({ msg: 'Failed to set user avatar from pending URL', name, url }); } } finally { await this.addCountCompleted(1); } } catch (error) { - this.logger.error(error); + this.logger.error({ msg: 'Failed to process pending avatar for user', err: error }); } } } catch (error) { diff --git a/apps/meteor/app/importer-slack/server/SlackImporter.ts b/apps/meteor/app/importer-slack/server/SlackImporter.ts index f0fefed3f749e..87098c8b35ec2 100644 --- a/apps/meteor/app/importer-slack/server/SlackImporter.ts +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -125,7 +125,7 @@ export class SlackImporter extends Importer { (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, ); - this.logger.debug(`loaded ${data.length} channels.`); + this.logger.debug({ msg: 'loaded channels', count: data.length }); await this.addCountToTotal(data.length); @@ -155,7 +155,7 @@ export class SlackImporter extends Importer { (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, ); - this.logger.debug(`loaded ${data.length} groups.`); + this.logger.debug({ msg: 'loaded groups', count: data.length }); await this.addCountToTotal(data.length); @@ -184,7 +184,7 @@ export class SlackImporter extends Importer { (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, ); - this.logger.debug(`loaded ${data.length} mpims.`); + this.logger.debug({ msg: 'loaded mpims', count: data.length }); await this.addCountToTotal(data.length); @@ -213,7 +213,7 @@ export class SlackImporter extends Importer { await super.updateProgress(ProgressStep.PREPARING_CHANNELS); const data = JSON.parse(entry.getData().toString()) as SlackChannel[]; - this.logger.debug(`loaded ${data.length} dms.`); + this.logger.debug({ msg: 'loaded dms', count: data.length }); await this.addCountToTotal(data.length); for await (const channel of data) { @@ -232,7 +232,7 @@ export class SlackImporter extends Importer { await super.updateProgress(ProgressStep.PREPARING_USERS); const data = JSON.parse(entry.getData().toString()) as SlackUser[]; - this.logger.debug(`loaded ${data.length} users.`); + this.logger.debug({ msg: 'loaded users', count: data.length }); // Insert the users record await this.updateRecord({ 'count.users': data.length }); @@ -352,7 +352,7 @@ export class SlackImporter extends Importer { try { if (entry.entryName.includes('__MACOSX') || entry.entryName.includes('.DS_Store')) { count++; - this.logger.debug(`Ignoring the file: ${entry.entryName}`); + this.logger.debug({ msg: 'Ignoring the file', entryName: entry.entryName }); continue; } @@ -385,7 +385,7 @@ export class SlackImporter extends Importer { } } } catch (error) { - this.logger.warn(`${entry.entryName} is not a valid JSON file! Unable to import it.`); + this.logger.warn({ msg: 'Entry is not a valid JSON file; unable to import', entryName: entry.entryName, err: error }); } } } catch (e) { @@ -626,7 +626,10 @@ export class SlackImporter extends Importer { newMessage.replies = Array.from(replies); } } else { - this.logger.warn(`Failed to import the parent comment, message: ${newMessage._id}. Missing replies/reply_users field`); + this.logger.warn({ + msg: 'Failed to import the parent comment; missing replies/reply_users field', + messageId: newMessage._id, + }); } newMessage.tcount = message.reply_count; diff --git a/apps/meteor/app/importer/server/classes/Importer.ts b/apps/meteor/app/importer/server/classes/Importer.ts index 5629daab05086..c39da71ed2a31 100644 --- a/apps/meteor/app/importer/server/classes/Importer.ts +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -71,7 +71,7 @@ export class Importer { this._lastProgressReportTotal = 0; this.reloadCount(); - this.logger.debug(`Constructed a new ${this.info.name} Importer.`); + this.logger.debug({ msg: 'Constructed a new Importer.', importerName: this.info.name }); } /** @@ -220,14 +220,14 @@ export class Importer { await this.updateProgress(ProgressStep.DONE); } catch (e) { - this.logger.error(e); + this.logger.error({ msg: 'Importer encountered an error during import', err: e }); await this.updateProgress(ProgressStep.ERROR); } finally { await this.applySettingValues(this.oldSettings); } const timeTook = Date.now() - started; - this.logger.log(`Import took ${timeTook} milliseconds.`); + this.logger.log({ msg: 'Import completed', durationMs: timeTook }); }); return this.getProgress(); @@ -279,7 +279,7 @@ export class Importer { async updateProgress(step: IImportProgress['step']): Promise { this.progress.step = step; - this.logger.debug(`${this.info.name} is now at ${step}.`); + this.logger.debug({ msg: 'Importer progress step updated', importerName: this.info.name, step }); await this.updateRecord({ status: this.progress.step }); // Do not send the default progress report during the preparing stage - the classes are sending their own report in a different format. @@ -343,7 +343,11 @@ export class Importer { }, 250); } - this.logger.log(`${this.progress.count.completed} records imported, ${this.progress.count.error} failed`); + this.logger.log({ + msg: 'Import progress update', + completed: this.progress.count.completed, + failed: this.progress.count.error, + }); return this.progress; } diff --git a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts index d530b96212d71..4b3658febde3b 100644 --- a/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RecordConverter.ts @@ -108,7 +108,11 @@ export class RecordConverter { - this._logger.error(error); + this._logger.error({ + msg: 'Import record conversion failed', + importId, + err: error, + }); this.saveErrorToMemory(importId, error); if (!this._converterOptions.workInMemory) { diff --git a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts index a19d5f0ae0fd3..97ed061ec8377 100644 --- a/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts +++ b/apps/meteor/app/importer/server/classes/converters/RoomConverter.ts @@ -98,7 +98,10 @@ export class RoomConverter extends RecordConverter { if (roomData.t === 'd') { if (members.length < roomData.users.length) { - this._logger.warn(`One or more imported users not found: ${roomData.users}`); + this._logger.warn({ + msg: 'One or more imported users not found', + users: roomData.users, + }); throw new Error('importer-channel-missing-users'); } } @@ -125,8 +128,7 @@ export class RoomConverter extends RecordConverter { roomData._id = roomInfo.rid; } catch (e) { - this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members }); - this._logger.error(e); + this._logger.warn({ msg: 'Failed to create new room', name: roomData.name, members, err: e }); throw e; } diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index a9b77aadf65bb..00bf1d911f565 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -318,7 +318,7 @@ class WebHookAPI extends APIClass<'/hooks'> { const integration = await Integrations.findOneByIdAndToken(integrationId, decodeURIComponent(token)); if (!integration) { - incomingLogger.info(`Invalid integration id ${integrationId} or token ${token}`); + incomingLogger.info({ msg: 'Invalid integration id or token', integrationId, token }); throw new Error('Invalid integration id or token provided.'); } From 5fa150953b86ff36face25083ed49e8c97a8044d Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 26 Dec 2025 09:16:03 -0300 Subject: [PATCH 012/104] fix: guest room limit incorrectly counting DMs (#37919) --- .changeset/strange-ants-impress.md | 7 +++++ apps/meteor/ee/app/license/server/startup.ts | 4 ++- .../ee/server/startup/maxRoomsPerGuest.ts | 4 ++- ee/packages/license/src/license.spec.ts | 26 +++++++++++++++++++ .../src/models/ISubscriptionsModel.ts | 2 +- packages/models/src/models/Subscriptions.ts | 7 +++-- 6 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 .changeset/strange-ants-impress.md diff --git a/.changeset/strange-ants-impress.md b/.changeset/strange-ants-impress.md new file mode 100644 index 0000000000000..b761a12b9af47 --- /dev/null +++ b/.changeset/strange-ants-impress.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/model-typings': patch +'@rocket.chat/models': patch +'@rocket.chat/meteor': patch +--- + +Makes roomsPerGuest exclude DMs when counting subscriptions, ensuring guest limits apply only to non-DM rooms as per expected behavior. diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts index b81dc8191b43c..f2e5768c96397 100644 --- a/apps/meteor/ee/app/license/server/startup.ts +++ b/apps/meteor/ee/app/license/server/startup.ts @@ -107,7 +107,9 @@ export const startLicense = async () => { License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); - License.setLicenseLimitCounter('roomsPerGuest', async (context) => (context?.userId ? Subscriptions.countByUserId(context.userId) : 0)); + License.setLicenseLimitCounter('roomsPerGuest', async (context) => + context?.userId ? Subscriptions.countByUserIdExceptType(context.userId, 'd') : 0, + ); License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM'))); diff --git a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts index 4172e46b594a6..3b421accbf9f5 100644 --- a/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts +++ b/apps/meteor/ee/server/startup/maxRoomsPerGuest.ts @@ -8,7 +8,9 @@ callbacks.add( 'beforeAddedToRoom', async ({ user }) => { if (user.roles?.includes('guest')) { - if (await License.shouldPreventAction('roomsPerGuest', 0, { userId: user._id })) { + // extraCount = 1 checks if adding one more room would exceed the limit + // (not if they've already exceeded it, since this runs before adding them to the room) + if (await License.shouldPreventAction('roomsPerGuest', 1, { userId: user._id })) { throw new Meteor.Error('error-max-rooms-per-guest-reached', i18n.t('error-max-rooms-per-guest-reached')); } } diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts index 63463a5b5b6c3..378591215c797 100644 --- a/ee/packages/license/src/license.spec.ts +++ b/ee/packages/license/src/license.spec.ts @@ -271,6 +271,32 @@ describe('Validate License Limits', () => { expect(fairUsageCallback).toHaveBeenCalledTimes(0); expect(preventActionCallback).toHaveBeenCalledTimes(0); }); + + it('should check roomsPerGuest with per-user context', async () => { + const licenseManager = await getReadyLicenseManager(); + const license = new MockedLicenseBuilder().withLimits('roomsPerGuest', [ + { + max: 3, + behavior: 'prevent_action', + }, + ]); + + await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true); + + licenseManager.setLicenseLimitCounter('roomsPerGuest', (context) => { + switch (context?.userId) { + case 'user1': + return 2; + case 'user2': + return 3; + default: + return 0; + } + }); + + await expect(licenseManager.shouldPreventAction('roomsPerGuest', 1, { userId: 'user1' })).resolves.toBe(false); + await expect(licenseManager.shouldPreventAction('roomsPerGuest', 1, { userId: 'user2' })).resolves.toBe(true); + }); }); }); diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 7958feae702e6..d28fb1be57e53 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -329,7 +329,7 @@ export interface ISubscriptionsModel extends IBaseModel { countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; countByRoomId(roomId: string, options?: CountDocumentsOptions): Promise; - countByUserId(userId: string): Promise; + countByUserIdExceptType(userId: string, typeException: ISubscription['t']): Promise; openByRoomIdAndUserId(roomId: string, userId: string): Promise; countByRoomIdAndNotUserId(rid: string, uid: string): Promise; countByRoomIdWhenUsernameExists(rid: string): Promise; diff --git a/packages/models/src/models/Subscriptions.ts b/packages/models/src/models/Subscriptions.ts index e73a4c1913421..520fcd8b3b66a 100644 --- a/packages/models/src/models/Subscriptions.ts +++ b/packages/models/src/models/Subscriptions.ts @@ -1172,8 +1172,11 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.countDocuments(query); } - countByUserId(userId: string): Promise { - const query = { 'u._id': userId }; + countByUserIdExceptType(userId: string, typeException: ISubscription['t']): Promise { + const query: Filter = { + 'u._id': userId, + 't': { $ne: typeException }, + }; return this.countDocuments(query); } From c8050708a067067a382770fd12eb15701e72c681 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Fri, 26 Dec 2025 10:05:47 -0300 Subject: [PATCH 013/104] feat: update name references on oauth login (#37954) --- .changeset/gold-trainers-shake.md | 5 ++ .../server/custom_oauth_server.js | 56 ++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 .changeset/gold-trainers-shake.md diff --git a/.changeset/gold-trainers-shake.md b/.changeset/gold-trainers-shake.md new file mode 100644 index 0000000000000..a8fcf48254df3 --- /dev/null +++ b/.changeset/gold-trainers-shake.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Changes OAuth login process to update users' names throughout the whole workspace when an existing user logs in with a changed name diff --git a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js index 4a5f6779974e5..f25405c8b14a0 100644 --- a/apps/meteor/app/custom-oauth/server/custom_oauth_server.js +++ b/apps/meteor/app/custom-oauth/server/custom_oauth_server.js @@ -11,7 +11,9 @@ import _ from 'underscore'; import { normalizers, fromTemplate, renameInvalidProperties } from './transform_helpers'; import { isURL } from '../../../lib/utils/isURL'; +import { client } from '../../../server/database/utils'; import { callbacks } from '../../../server/lib/callbacks'; +import { saveUserIdentity } from '../../lib/server/functions/saveUserIdentity'; import { notifyOnUserChange } from '../../lib/server/lib/notifyListener'; import { registerAccessTokenService } from '../../lib/server/oauth/oauth'; import { settings } from '../../settings/server'; @@ -366,17 +368,55 @@ export class CustomOAuth { } const serviceIdKey = `services.${serviceName}.id`; - const update = { - $set: { - name: serviceData.name, - ...(this.keyField === 'username' && serviceData.email && { emails: [{ address: serviceData.email, verified: true }] }), - [serviceIdKey]: serviceData.id, + const successCallbacks = [ + async () => { + const updatedUser = await Users.findOneById(user._id, { projection: { name: 1, emails: 1, [serviceIdKey]: 1 } }); + if (updatedUser) { + const { _id, ...diff } = updatedUser; + void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff }); + } }, - }; + ]; + + const session = client.startSession(); + try { + // Extend the session to match the ExtendedSession type expected by saveUserIdentity + Object.assign(session, { + onceSuccesfulCommit: (cb) => { + successCallbacks.push(cb); + }, + }); + + session.startTransaction(); + + const updater = Users.getUpdater(); - await Users.update({ _id: user._id }, update); + if (this.keyField === 'username' && serviceData.email) { + updater.set('emails', [{ address: serviceData.email, verified: true }]); + } + + updater.set(serviceIdKey, serviceData.id); + + await saveUserIdentity({ + _id: user._id, + name: serviceData.name, + updater, + session, + updateUsernameInBackground: true, + // Username needs to be included otherwise the name won't be updated in some collections + username: user.username, + }); + await Users.updateFromUpdater({ _id: user._id }, updater, { session }); + + await session.commitTransaction(); + } catch (e) { + await session.abortTransaction(); + throw e; + } finally { + await session.endSession(); + } - void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: update }); + void Promise.allSettled(successCallbacks.map((cb) => cb())); } }); From af0600c45643b46c6d61c8dfe5f7630538ff1673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Mon, 29 Dec 2025 14:10:01 -0300 Subject: [PATCH 014/104] test: Replace `data-qa` by accessible locators (#36757) --- .../client/components/UserCard/UserCard.tsx | 2 +- .../UserAvatarEditor/UserAvatarEditor.tsx | 4 +- .../actions/CreateChannelModal.tsx | 6 +- .../actions/CreateDirectMessage.tsx | 1 - .../RoomList/SidebarItemTemplateWithData.tsx | 1 - .../sidebar/RoomList/SidebarItemWithData.tsx | 1 - .../account/profile/AccountProfilePage.tsx | 9 +-- .../account/security/ChangePassphrase.tsx | 8 +-- .../account/security/ResetPassphrase.tsx | 4 +- .../EngagementDashboardCardErrorBoundary.tsx | 2 +- .../admin/permissions/PermissionsPage.tsx | 8 +-- .../PermissionsTableFilter.tsx | 10 +-- .../PermissionsTable.spec.tsx.snap | 2 - .../client/views/home/CustomHomePage.tsx | 2 +- .../client/views/home/DefaultHomePage.tsx | 4 +- .../client/views/home/HomePageHeader.tsx | 2 +- .../AppInstallModal/AppInstallModal.tsx | 2 +- .../sidebar/RoomList/SidebarItemWithData.tsx | 1 - .../additionalForms/AutoCompleteUnits.tsx | 1 - .../views/omnichannel/agents/AgentEdit.tsx | 6 +- .../views/omnichannel/agents/AgentInfo.tsx | 10 +-- .../components/AutoCompleteMultipleAgent.tsx | 1 - .../customFields/CustomFieldsTable.tsx | 2 +- .../customFields/EditCustomFields.tsx | 2 +- .../DepartmentAgentsTable.tsx | 2 +- .../directory/chats/ChatsTable/ChatsTable.tsx | 2 +- .../components/ReportCardErrorState.tsx | 2 +- .../RoomMembers/RoomMembersActions.tsx | 2 +- .../UserInfo/UserInfoActions.tsx | 2 - apps/meteor/tests/e2e/admin-room.spec.ts | 3 +- .../page-objects/account-profile.ts | 2 +- .../page-objects/fragments/home-content.ts | 4 -- .../fragments/home-flextab-members.ts | 2 +- apps/meteor/tests/e2e/homepage.spec.ts | 16 ++--- .../tests/e2e/page-objects/account-profile.ts | 8 +-- .../e2e/page-objects/account-security.ts | 2 +- .../page-objects/fragments/home-content.ts | 2 +- .../page-objects/fragments/home-sidenav.ts | 62 ------------------- .../tests/e2e/page-objects/fragments/index.ts | 1 - .../tests/e2e/page-objects/home-channel.ts | 11 ++-- .../tests/e2e/page-objects/home-discussion.ts | 5 +- .../e2e/page-objects/omnichannel-room-info.ts | 8 +-- apps/meteor/tests/e2e/settings-assets.spec.ts | 4 +- apps/meteor/tests/e2e/settings-int.spec.ts | 3 +- packages/i18n/src/locales/en.i18n.json | 2 + .../components/Page/PageHeaderNoShadow.tsx | 2 +- 46 files changed, 64 insertions(+), 174 deletions(-) delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts diff --git a/apps/meteor/client/components/UserCard/UserCard.tsx b/apps/meteor/client/components/UserCard/UserCard.tsx index d8878524fce35..7482621c49ae4 100644 --- a/apps/meteor/client/components/UserCard/UserCard.tsx +++ b/apps/meteor/client/components/UserCard/UserCard.tsx @@ -49,7 +49,7 @@ const UserCard = ({ const isLayoutEmbedded = useEmbeddedLayout(); return ( - +
{username && } diff --git a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx index 4a80185a399c0..b88acc2781335 100644 --- a/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx +++ b/apps/meteor/client/components/avatar/UserAvatarEditor/UserAvatarEditor.tsx @@ -80,7 +80,7 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab size='x124' url={url} key={url} - data-qa-id='UserAvatarEditor' + alt={t('__username__profile_picture', { username: currentUsername || 'user' })} username={currentUsername || ''} etag={etag} style={{ @@ -102,7 +102,6 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab title={t('Add_URL')} mi={4} onClick={handleAddUrl} - data-qa-id='UserAvatarEditorSetAvatarLink' /> @@ -110,7 +109,6 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab {t('Use_url_for_avatar')} ) => ( @@ -226,7 +225,6 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh validateChannelName(value), @@ -248,7 +246,7 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh {t('Topic')} - + {t('Displayed_next_to_name')} @@ -407,7 +405,7 @@ const CreateChannelModal = ({ teamId = '', mainRoom, onClose, reload }: CreateCh - diff --git a/apps/meteor/client/navbar/NavBarPagesGroup/actions/CreateDirectMessage.tsx b/apps/meteor/client/navbar/NavBarPagesGroup/actions/CreateDirectMessage.tsx index ddac0c930ae3c..59318d02fa26b 100644 --- a/apps/meteor/client/navbar/NavBarPagesGroup/actions/CreateDirectMessage.tsx +++ b/apps/meteor/client/navbar/NavBarPagesGroup/actions/CreateDirectMessage.tsx @@ -60,7 +60,6 @@ const CreateDirectMessage = ({ onClose }: CreateDirectMessageProps) => { return ( } > diff --git a/apps/meteor/client/sidebar/RoomList/SidebarItemTemplateWithData.tsx b/apps/meteor/client/sidebar/RoomList/SidebarItemTemplateWithData.tsx index 331a5b8ce44a8..6e67ed9334fb4 100644 --- a/apps/meteor/client/sidebar/RoomList/SidebarItemTemplateWithData.tsx +++ b/apps/meteor/client/sidebar/RoomList/SidebarItemTemplateWithData.tsx @@ -125,7 +125,6 @@ const SidebarItemTemplateWithData = ({ { - diff --git a/apps/meteor/client/views/account/security/ChangePassphrase.tsx b/apps/meteor/client/views/account/security/ChangePassphrase.tsx index 752d34ee6f47e..8dab8d387b866 100644 --- a/apps/meteor/client/views/account/security/ChangePassphrase.tsx +++ b/apps/meteor/client/views/account/security/ChangePassphrase.tsx @@ -196,13 +196,7 @@ export const ChangePassphrase = (): JSX.Element => { )} - diff --git a/apps/meteor/client/views/account/security/ResetPassphrase.tsx b/apps/meteor/client/views/account/security/ResetPassphrase.tsx index 83077292c8732..6e01f74f311d4 100644 --- a/apps/meteor/client/views/account/security/ResetPassphrase.tsx +++ b/apps/meteor/client/views/account/security/ResetPassphrase.tsx @@ -14,9 +14,7 @@ export const ResetPassphrase = (): JSX.Element => { {t('Reset_E2EE_password_description')} - + ); }; diff --git a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx index aee0412283802..dcfe34ff50e2f 100644 --- a/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx +++ b/apps/meteor/client/views/admin/engagementDashboard/EngagementDashboardCardErrorBoundary.tsx @@ -32,7 +32,7 @@ const EngagementDashboardCardErrorBoundary = ({ children }: EngagementDashboardC {t('Something_went_wrong')} {isError(error) && error?.message} - + resetErrorBoundary()}>{t('Retry')} diff --git a/apps/meteor/client/views/admin/permissions/PermissionsPage.tsx b/apps/meteor/client/views/admin/permissions/PermissionsPage.tsx index feb34c289acbd..d245d4e394328 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsPage.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsPage.tsx @@ -59,19 +59,13 @@ const PermissionsPage = ({ isEnterprise }: { isEnterprise: boolean }): ReactElem {t('Permissions')} - + {t('Settings')} diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx index a86f97d66e95b..76dffe51bffae 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx @@ -17,15 +17,7 @@ const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: stri setFilter(value); }); - return ( - - ); + return ; }; export default PermissionsTableFilter; diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/__snapshots__/PermissionsTable.spec.tsx.snap b/apps/meteor/client/views/admin/permissions/PermissionsTable/__snapshots__/PermissionsTable.spec.tsx.snap index c896af1674544..597ba10319d9d 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/__snapshots__/PermissionsTable.spec.tsx.snap +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/__snapshots__/PermissionsTable.spec.tsx.snap @@ -8,7 +8,6 @@ exports[`renders Default without crashing 1`] = ` > { return ( - + diff --git a/apps/meteor/client/views/home/DefaultHomePage.tsx b/apps/meteor/client/views/home/DefaultHomePage.tsx index 1c8e5fd680dec..fa394949c4f4b 100644 --- a/apps/meteor/client/views/home/DefaultHomePage.tsx +++ b/apps/meteor/client/views/home/DefaultHomePage.tsx @@ -24,10 +24,10 @@ const DefaultHomePage = (): ReactElement => { const isCustomContentVisible = useSetting('Layout_Home_Custom_Block_Visible', false); return ( - + - + {t('Welcome_to_workspace', { Site_Name: workspaceName || 'Rocket.Chat' })} diff --git a/apps/meteor/client/views/home/HomePageHeader.tsx b/apps/meteor/client/views/home/HomePageHeader.tsx index a8a46043f51d7..85018f42ee805 100644 --- a/apps/meteor/client/views/home/HomePageHeader.tsx +++ b/apps/meteor/client/views/home/HomePageHeader.tsx @@ -12,7 +12,7 @@ const HomepageHeader = (): ReactElement => { const settingsRoute = useRoute('admin-settings'); return ( - + {canEditLayout && ( - diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx index 1ace56dd02ca2..b6cd94e4cb2f6 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentAgentsTable/DepartmentAgentsTable.tsx @@ -25,7 +25,7 @@ function DepartmentAgentsTable({ control, register, 'aria-labelledby': ariaLabel return ( <> - + diff --git a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTable.tsx b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTable.tsx index b5590ea120c5d..7bcc9655f06b5 100644 --- a/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/chats/ChatsTable/ChatsTable.tsx @@ -63,7 +63,7 @@ const ChatsTable = () => { {t('Last_Message')} {t('Status')} - {canRemoveClosedChats && } + {canRemoveClosedChats && } ); diff --git a/apps/meteor/client/views/omnichannel/reports/components/ReportCardErrorState.tsx b/apps/meteor/client/views/omnichannel/reports/components/ReportCardErrorState.tsx index 5dbe13ae06d37..f918ce2385173 100644 --- a/apps/meteor/client/views/omnichannel/reports/components/ReportCardErrorState.tsx +++ b/apps/meteor/client/views/omnichannel/reports/components/ReportCardErrorState.tsx @@ -13,7 +13,7 @@ export const ReportCardErrorState = ({ onRetry }: ReportCardErrorStateProps): Re {t('Something_went_wrong')} - + {t('Retry')} diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx index c53892d42252d..5af532ac1edb8 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersActions.tsx @@ -34,7 +34,7 @@ const RoomMembersActions = ({ if (!menuOptions) { return null; } - return ; + return ; }; export default RoomMembersActions; diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx index ca7c6b0c88531..929c1d9c7570c 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx @@ -51,11 +51,9 @@ const UserInfoActions = ({ user, rid, isInvited, backToList }: UserInfoActionsPr button={} title={t('More')} key='menu' - data-qa-id='UserUserInfo-menu' sections={menuOptions} placement='bottom-end' small={false} - data-qa='UserUserInfo-menu' /> ); }, [menuOptions, t]); diff --git a/apps/meteor/tests/e2e/admin-room.spec.ts b/apps/meteor/tests/e2e/admin-room.spec.ts index 6c9a4f4755090..73306ba016017 100644 --- a/apps/meteor/tests/e2e/admin-room.spec.ts +++ b/apps/meteor/tests/e2e/admin-room.spec.ts @@ -23,7 +23,8 @@ test.describe.serial('admin-rooms', () => { }); test('should display the Rooms Table', async ({ page }) => { - await expect(page.locator('[data-qa-type="PageHeader-title"]')).toContainText('Rooms'); + await expect(page.getByRole('main').getByRole('heading', { level: 1, name: 'Rooms', exact: true })).toBeVisible(); + await expect(page.getByRole('main').getByRole('table')).toBeVisible(); }); test('should filter room by name', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts b/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts index 7b51e1ddc3492..631eedd10f77f 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/account-profile.ts @@ -12,6 +12,6 @@ export class FederationAccountProfile { } get btnSubmit(): Locator { - return this.page.locator('[data-qa="AccountProfilePageSaveButton"]'); + return this.page.getByRole('button', { name: 'Save changes', exact: true }); } } diff --git a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts index 1f93afcd9cb77..a60c5c4955b92 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts @@ -150,10 +150,6 @@ export class FederationHomeContent { return this.page.locator('[data-qa-id="menu-more-actions"]'); } - get linkUserCard(): Locator { - return this.page.locator('[data-qa="UserCard"] a'); - } - get btnContactEdit(): Locator { return this.page.locator('.rcx-vertical-bar button:has-text("Edit")'); } diff --git a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab-members.ts b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab-members.ts index 69d7b5a743d9f..937e55bf8d920 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab-members.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-flextab-members.ts @@ -20,7 +20,7 @@ export class FederationHomeFlextabMembers { } get btnMenuUserInfo(): Locator { - return this.page.locator('[data-qa="UserUserInfo-menu"]'); + return this.page.getByRole('dialog', { name: 'User Info', exact: true }).getByRole('button', { name: 'More', exact: true }); } getKebabMenuForUser(username: string): Locator { diff --git a/apps/meteor/tests/e2e/homepage.spec.ts b/apps/meteor/tests/e2e/homepage.spec.ts index 72291785bd09c..2543e66af8177 100644 --- a/apps/meteor/tests/e2e/homepage.spec.ts +++ b/apps/meteor/tests/e2e/homepage.spec.ts @@ -23,7 +23,7 @@ test.describe.serial('homepage', () => { test.beforeAll(async ({ browser }) => { adminPage = await browser.newPage({ storageState: Users.admin.state }); await adminPage.goto('/home'); - await adminPage.waitForSelector('[data-qa-id="home-header"]'); + await adminPage.getByRole('main').getByRole('heading', { level: 1, name: 'Home', exact: true }).waitFor(); }); test.afterAll(async ({ api }) => { @@ -113,7 +113,7 @@ test.describe.serial('homepage', () => { expect((await api.post('/settings/Layout_Home_Body', { value: '' })).status()).toBe(200); regularUserPage = await browser.newPage({ storageState: Users.user2.state }); await regularUserPage.goto('/home'); - await regularUserPage.waitForSelector('[data-qa-id="home-header"]'); + await regularUserPage.getByRole('main').getByRole('heading', { level: 1, name: 'Home', exact: true }).waitFor(); }); test.afterAll(async () => { @@ -142,7 +142,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect header text to use Layout_Home_Title default setting', async () => { - await expect(regularUserPage.locator('[data-qa-type="PageHeader-title"]')).toContainText('Home'); + await expect(regularUserPage.getByRole('main').getByRole('heading', { level: 1, name: 'Home', exact: true })).toBeVisible(); }); }); @@ -152,7 +152,7 @@ test.describe.serial('homepage', () => { expect((await api.post('/settings/Layout_Home_Title', { value: 'NewTitle' })).status()).toBe(200); await regularUserPage.goto('/home'); - await regularUserPage.waitForSelector('[data-qa-id="home-header"]'); + await regularUserPage.getByRole('main').getByRole('heading', { level: 1, name: 'NewTitle', exact: true }).waitFor(); }); test.afterAll(async ({ api }) => { @@ -166,7 +166,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect header text to be Layout_Home_Title setting', async () => { - await expect(regularUserPage.locator('[data-qa-type="PageHeader-title"]')).toContainText('NewTitle'); + await expect(regularUserPage.getByRole('main').getByRole('heading', { name: 'NewTitle', exact: true })).toBeVisible(); }); }); }); @@ -177,7 +177,7 @@ test.describe.serial('homepage', () => { expect((await api.post('/settings/Layout_Home_Custom_Block_Visible', { value: true })).status()).toBe(200); await regularUserPage.goto('/home'); - await regularUserPage.waitForSelector('[data-qa-id="home-header"]'); + await regularUserPage.getByRole('main').getByRole('heading', { level: 1, name: 'Home', exact: true }).waitFor(); }); test.afterAll(async ({ api }) => { @@ -202,7 +202,9 @@ test.describe.serial('homepage', () => { test('expect default layout not be visible and custom body visible', async () => { await test.step('expect default layout to not be visible', async () => { - await expect(regularUserPage.locator('[data-qa-id="homepage-welcome-text"]')).not.toBeVisible(); + await expect( + regularUserPage.getByRole('main').getByRole('heading', { level: 2, name: 'Welcome to Rocket.chat', exact: true }), + ).not.toBeVisible(); }); await test.step('expect custom body to be visible', async () => { diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index 0b6a47fe1abe6..84b391a292c60 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -12,11 +12,11 @@ export class AccountProfile extends Account { } get inputAvatarLink(): Locator { - return this.page.locator('[data-qa-id="UserAvatarEditorLink"]'); + return this.page.getByRole('textbox', { name: 'Use URL for avatar' }); } get btnSetAvatarLink(): Locator { - return this.page.locator('[data-qa-id="UserAvatarEditorSetAvatarLink"]'); + return this.page.getByRole('button', { name: 'Add URL', exact: true }); } get inputUsername(): Locator { @@ -25,7 +25,7 @@ export class AccountProfile extends Account { // TODO: remove this locator get btnSubmit(): Locator { - return this.page.locator('[data-qa="AccountProfilePageSaveButton"]'); + return this.page.getByRole('button', { name: 'Save changes', exact: true }); } get avatarFileInput(): Locator { @@ -33,7 +33,7 @@ export class AccountProfile extends Account { } get userAvatarEditor(): Locator { - return this.page.locator('[data-qa-id="UserAvatarEditor"]'); + return this.page.getByAltText('profile picture'); } get emailTextInput(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/account-security.ts b/apps/meteor/tests/e2e/page-objects/account-security.ts index dfeb2ff8ec5cd..a54813eacbdb1 100644 --- a/apps/meteor/tests/e2e/page-objects/account-security.ts +++ b/apps/meteor/tests/e2e/page-objects/account-security.ts @@ -40,7 +40,7 @@ export class AccountSecurity extends Account { } get securityHeader(): Locator { - return this.page.locator('h1[data-qa-type="PageHeader-title"]:has-text("Security")'); + return this.page.getByRole('main').getByRole('heading', { level: 1, name: 'Security', exact: true }); } get securityPasswordSection(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index 3eafb1f99a15a..e5b1741d3047a 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -312,7 +312,7 @@ export class HomeContent { } get userCard(): Locator { - return this.page.locator('[data-qa="UserCard"]'); + return this.page.getByRole('dialog', { name: 'User card', exact: true }); } get linkUserCard(): Locator { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts deleted file mode 100644 index 34b38c60c6280..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class HomeSidenav { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get advancedSettingsAccordion(): Locator { - return this.page.getByRole('dialog').getByRole('button', { name: 'Advanced settings', exact: true }); - } - - get checkboxPrivateChannel(): Locator { - return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Private' }) }); - } - - get checkboxEncryption(): Locator { - return this.page.locator('role=dialog[name="Create channel"] >> label >> text="Encrypted"'); - } - - get checkboxReadOnly(): Locator { - return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Read-only' }) }); - } - - get inputChannelName(): Locator { - return this.page.locator('#modal-root [data-qa="create-channel-modal"] [data-qa-type="channel-name-input"]'); - } - - get inputDirectUsername(): Locator { - return this.page.locator('#modal-root [data-qa="create-direct-modal"] [data-qa-type="user-auto-complete-input"]'); - } - - get btnCreate(): Locator { - return this.page.locator('role=button[name="Create"]'); - } - - get inputSearch(): Locator { - return this.page.locator('role=search >> role=searchbox').first(); - } - - get sidebarChannelsList(): Locator { - return this.page.getByRole('list', { name: 'Channels' }); - } - - get sidebarToolbar(): Locator { - return this.page.getByRole('toolbar', { name: 'Sidebar actions' }); - } - - // Note: this is different from openChat because queued chats are not searchable - getQueuedChat(name: string): Locator { - return this.page.locator('[data-qa="sidebar-item-title"]', { hasText: new RegExp(`^${name}$`) }).first(); - } - - getSidebarItemByName(name: string): Locator { - return this.page.getByRole('link').filter({ has: this.page.getByText(name, { exact: true }) }); - } - - get homepageHeader(): Locator { - return this.page.locator('main').getByRole('heading', { name: 'Home' }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index 18053e6b80bde..51ba6595b6d06 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -3,7 +3,6 @@ export * from './user-info-flextab'; export * from './home-content'; export * from './home-omnichannel-content'; export * from './home-flextab'; -export * from './home-sidenav'; export * from './omnichannel-sidenav'; export * from './navbar'; export * from './sidebar'; diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index 42f2316401f98..478ca3d2f84b6 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidepanel, RoomSidebar, ToastMessages } from './fragments'; +import { HomeContent, HomeFlextab, Navbar, Sidepanel, RoomSidebar, ToastMessages } from './fragments'; import { RoomToolbar } from './fragments/toolbar'; import { VoiceCalls } from './fragments/voice-calls'; @@ -9,8 +9,6 @@ export class HomeChannel { readonly content: HomeContent; - readonly sidenav: HomeSidenav; - readonly sidebar: RoomSidebar; readonly sidepanel: Sidepanel; @@ -28,7 +26,6 @@ export class HomeChannel { constructor(page: Page) { this.page = page; this.content = new HomeContent(page); - this.sidenav = new HomeSidenav(page); this.sidebar = new RoomSidebar(page); this.sidepanel = new Sidepanel(page); this.navbar = new Navbar(page); @@ -122,8 +119,12 @@ export class HomeChannel { return this.page.getByRole('main').getByRole('status'); } + get homepageHeader(): Locator { + return this.page.locator('main').getByRole('heading', { name: 'Home' }); + } + async waitForHome(): Promise { - await this.sidenav.homepageHeader.waitFor({ state: 'visible' }); + await this.homepageHeader.waitFor({ state: 'visible' }); } async waitForRoomLoad(): Promise { diff --git a/apps/meteor/tests/e2e/page-objects/home-discussion.ts b/apps/meteor/tests/e2e/page-objects/home-discussion.ts index 040b4436e8c95..1088007bd9152 100644 --- a/apps/meteor/tests/e2e/page-objects/home-discussion.ts +++ b/apps/meteor/tests/e2e/page-objects/home-discussion.ts @@ -1,14 +1,12 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab, Navbar } from './fragments'; +import { HomeContent, HomeFlextab, Navbar } from './fragments'; export class HomeDiscussion { private readonly page: Page; readonly content: HomeContent; - readonly sidenav: HomeSidenav; - readonly navbar: Navbar; readonly tabs: HomeFlextab; @@ -16,7 +14,6 @@ export class HomeDiscussion { constructor(page: Page) { this.page = page; this.content = new HomeContent(page); - this.sidenav = new HomeSidenav(page); this.navbar = new Navbar(page); this.tabs = new HomeFlextab(page); } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts index 7c415faccdb70..e7f6430fc38e0 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-room-info.ts @@ -1,15 +1,15 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeSidenav } from './fragments/home-sidenav'; +import { RoomSidebar } from './fragments'; export class OmnichannelRoomInfo { private readonly page: Page; - private readonly homeSidenav: HomeSidenav; + private readonly sidebar: RoomSidebar; constructor(page: Page) { this.page = page; - this.homeSidenav = new HomeSidenav(page); + this.sidebar = new RoomSidebar(page); } get dialogRoomInfo(): Locator { @@ -70,6 +70,6 @@ export class OmnichannelRoomInfo { } getBadgeIndicator(name: string, title: string): Locator { - return this.homeSidenav.getSidebarItemByName(name).getByTitle(title); + return this.sidebar.getSidebarItemByName(name).getByTitle(title); } } diff --git a/apps/meteor/tests/e2e/settings-assets.spec.ts b/apps/meteor/tests/e2e/settings-assets.spec.ts index bdc265b63dc3d..f2f83360567b8 100644 --- a/apps/meteor/tests/e2e/settings-assets.spec.ts +++ b/apps/meteor/tests/e2e/settings-assets.spec.ts @@ -12,8 +12,8 @@ test.describe.serial('settings-assets', () => { await page.goto('/admin/settings'); await poAdminSettings.btnAssetsSettings.click(); - // FIXME: This is not good practice. We should look for a better way to ensure the page is loaded - await expect(page.locator('[data-qa-type="PageHeader-title"]')).toHaveText('Assets'); + + await expect(page.getByRole('main').getByRole('heading', { level: 1, name: 'Assets', exact: true })).toBeVisible(); }); test('expect upload and delete logo asset and label should be visible', async ({ page }) => { diff --git a/apps/meteor/tests/e2e/settings-int.spec.ts b/apps/meteor/tests/e2e/settings-int.spec.ts index 797e55c82a4aa..b7ac6c4c25eab 100644 --- a/apps/meteor/tests/e2e/settings-int.spec.ts +++ b/apps/meteor/tests/e2e/settings-int.spec.ts @@ -9,10 +9,11 @@ test.describe.serial('settings-int', () => { test.beforeEach(async ({ page }) => { poAdminSettings = new AdminSettings(page); - const pageTitle = page.locator('[data-qa-type="PageHeader-title"]'); + const pageTitle = page.getByRole('main').getByRole('heading', { level: 1, name: 'Message', exact: true }); await page.goto('/admin/settings/Message'); await pageTitle.waitFor(); + await expect(pageTitle).toBeVisible(); await expect(pageTitle).toHaveText('Message'); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 887eb27955fad..ebde7631d98ce 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -7081,5 +7081,7 @@ "timestamps.longDateDescription": "12/31/2020, 12:00 AM", "timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM", "timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM", + "__username__profile_picture": "{{username}}'s profile picture", + "User_card": "User card", "timestamps.relativeTimeDescription": "1 year ago" } \ No newline at end of file diff --git a/packages/ui-client/src/components/Page/PageHeaderNoShadow.tsx b/packages/ui-client/src/components/Page/PageHeaderNoShadow.tsx index 188900d573cb4..d00cfbdec13ea 100644 --- a/packages/ui-client/src/components/Page/PageHeaderNoShadow.tsx +++ b/packages/ui-client/src/components/Page/PageHeaderNoShadow.tsx @@ -37,7 +37,7 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props ) : null} {onClickBack && } - + {title} {children} From e1e6f7be40bb570f3491a7ba46012585249639f6 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:43:26 -0300 Subject: [PATCH 015/104] chore: improve handling of unused streams on media calls (#37921) --- .../src/definition/call/IClientMediaCall.ts | 2 +- packages/media-signaling/src/lib/Call.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/media-signaling/src/definition/call/IClientMediaCall.ts b/packages/media-signaling/src/definition/call/IClientMediaCall.ts index 40d061f27079d..b39feac23eea4 100644 --- a/packages/media-signaling/src/definition/call/IClientMediaCall.ts +++ b/packages/media-signaling/src/definition/call/IClientMediaCall.ts @@ -97,7 +97,7 @@ export interface IClientMediaCall { emitter: Emitter; - getRemoteMediaStream(): MediaStream; + getRemoteMediaStream(): MediaStream | null; accept(): void; reject(): void; diff --git a/packages/media-signaling/src/lib/Call.ts b/packages/media-signaling/src/lib/Call.ts index d825fadb4e3d5..fe0ff5a705fcc 100644 --- a/packages/media-signaling/src/lib/Call.ts +++ b/packages/media-signaling/src/lib/Call.ts @@ -438,14 +438,14 @@ export class ClientMediaCall implements IClientMediaCall { } } - public getRemoteMediaStream(): MediaStream { + public getRemoteMediaStream(): MediaStream | null { this.config.logger?.debug('ClientMediaCall.getRemoteMediaStream'); - if (this.hidden) { - this.throwError('getRemoteMediaStream is not available for this call'); + if (this.hidden || !this.signed) { + return null; } if (this.shouldIgnoreWebRTC()) { - this.throwError('getRemoteMediaStream is not available for this service'); + return null; } this.prepareWebRtcProcessor(); From 17ed52a9ed2f606fd5d6f0bc9c3abb3ce62785ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:25:29 -0300 Subject: [PATCH 016/104] test: remove homepage `data-qa` attributes (#38004) --- .../client/views/home/cards/AddUsersCard.tsx | 1 - .../views/home/cards/CreateChannelsCard.tsx | 1 - .../views/home/cards/CustomContentCard.tsx | 7 +++-- .../views/home/cards/DesktopAppsCard.tsx | 1 - .../views/home/cards/DocumentationCard.tsx | 1 - .../client/views/home/cards/JoinRoomsCard.tsx | 1 - .../views/home/cards/MobileAppsCard.tsx | 1 - apps/meteor/tests/e2e/homepage.spec.ts | 28 +++++++++---------- apps/meteor/tests/e2e/settings-int.spec.ts | 1 - packages/i18n/src/locales/en.i18n.json | 1 + 10 files changed, 19 insertions(+), 24 deletions(-) diff --git a/apps/meteor/client/views/home/cards/AddUsersCard.tsx b/apps/meteor/client/views/home/cards/AddUsersCard.tsx index 6ae6fe0e9513c..bc5265bfac083 100644 --- a/apps/meteor/client/views/home/cards/AddUsersCard.tsx +++ b/apps/meteor/client/views/home/cards/AddUsersCard.tsx @@ -17,7 +17,6 @@ const AddUsersCard = (props: Omit, 'type'>): ReactEl title={t('Add_users')} body={t('Invite_and_add_members_to_this_workspace_to_start_communicating')} buttons={[]} - data-qa-id='homepage-add-users-card' width='x340' {...props} /> diff --git a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx index fe14565fb3530..ed363c51fae95 100644 --- a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx +++ b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx @@ -16,7 +16,6 @@ const CreateChannelsCard = (props: Omit, 'type'>): R title={t('Create_channels')} body={t('Create_a_public_channel_that_new_workspace_members_can_join')} buttons={[]} - data-qa-id='homepage-create-channels-card' width='x340' {...props} /> diff --git a/apps/meteor/client/views/home/cards/CustomContentCard.tsx b/apps/meteor/client/views/home/cards/CustomContentCard.tsx index c68ab2bd21ef4..77a8d64192fff 100644 --- a/apps/meteor/client/views/home/cards/CustomContentCard.tsx +++ b/apps/meteor/client/views/home/cards/CustomContentCard.tsx @@ -1,12 +1,13 @@ import { Box, Button, Card, CardBody, CardControls, CardHeader, Icon, Tag } from '@rocket.chat/fuselage'; -import { useRole, useSettingSetValue, useSetting, useToastMessageDispatch, useTranslation, useRouter } from '@rocket.chat/ui-contexts'; +import { useRole, useSettingSetValue, useSetting, useToastMessageDispatch, useRouter } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import CustomHomepageContent from '../CustomHomePageContent'; const CustomContentCard = (props: Omit, 'type'>): ReactElement | null => { - const t = useTranslation(); + const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const router = useRouter(); @@ -50,7 +51,7 @@ const CustomContentCard = (props: Omit, 'type'>): Re if (isAdmin) { return ( - + diff --git a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx index 3fc4578a294c0..674385f6ca27d 100644 --- a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx +++ b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx @@ -24,7 +24,6 @@ const DesktopAppsCard = (props: Omit, 'type'>): Reac handleOpenLink(MAC_APP_URL)} children={t('Platform_Mac')} role='link' />, ]} width='x340' - data-qa-id='homepage-desktop-apps-card' {...props} /> ); diff --git a/apps/meteor/client/views/home/cards/DocumentationCard.tsx b/apps/meteor/client/views/home/cards/DocumentationCard.tsx index 8cb3b5452c6ca..104b2f1938660 100644 --- a/apps/meteor/client/views/home/cards/DocumentationCard.tsx +++ b/apps/meteor/client/views/home/cards/DocumentationCard.tsx @@ -17,7 +17,6 @@ const DocumentationCard = (props: Omit, 'type'>): Re title={t('Documentation')} body={t('Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat')} buttons={[ handleOpenLink(DOCS_URL)} children={t('See_documentation')} role='link' />]} - data-qa-id='homepage-documentation-card' width='x340' {...props} /> diff --git a/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx b/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx index 0fbd487c1d109..8ff00d2d5dc09 100644 --- a/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx +++ b/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx @@ -17,7 +17,6 @@ const JoinRoomsCard = (props: Omit, 'type'>): ReactE title={t('Join_rooms')} body={t('Discover_public_channels_and_teams_in_the_workspace_directory')} buttons={[]} - data-qa-id='homepage-join-rooms-card' width='x340' {...props} /> diff --git a/apps/meteor/client/views/home/cards/MobileAppsCard.tsx b/apps/meteor/client/views/home/cards/MobileAppsCard.tsx index 1a6b0f1c6eb34..e91cf3fabcfc3 100644 --- a/apps/meteor/client/views/home/cards/MobileAppsCard.tsx +++ b/apps/meteor/client/views/home/cards/MobileAppsCard.tsx @@ -21,7 +21,6 @@ const MobileAppsCard = (props: Omit, 'type'>): React handleOpenLink(GOOGLE_PLAY_URL)} children={t('Google_Play')} role='link' />, handleOpenLink(APP_STORE_URL)} children={t('App_Store')} role='link' />, ]} - data-qa-id='homepage-mobile-apps-card' width='x340' {...props} /> diff --git a/apps/meteor/tests/e2e/homepage.spec.ts b/apps/meteor/tests/e2e/homepage.spec.ts index 2543e66af8177..8c7eb6670d4cf 100644 --- a/apps/meteor/tests/e2e/homepage.spec.ts +++ b/apps/meteor/tests/e2e/homepage.spec.ts @@ -4,14 +4,14 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { expect, test } from './utils/test'; -const CardIds = { - Users: 'homepage-add-users-card', - Chan: 'homepage-create-channels-card', - Rooms: 'homepage-join-rooms-card', - Mobile: 'homepage-mobile-apps-card', - Desktop: 'homepage-desktop-apps-card', - Docs: 'homepage-documentation-card', - Custom: 'homepage-custom-card', +const CardNames = { + Users: 'Add users', + Chan: 'Create channels', + Rooms: 'Join rooms', + Mobile: 'Mobile apps', + Desktop: 'Desktop apps', + Docs: 'Documentation', + Custom: 'Custom content', }; test.use({ storageState: Users.admin.state }); @@ -38,7 +38,7 @@ test.describe.serial('homepage', () => { }); await test.step('expect all cards to be visible', async () => { - await Promise.all(Object.values(CardIds).map((id) => expect(adminPage.locator(`[data-qa-id="${id}"]`)).toBeVisible())); + await Promise.all(Object.values(CardNames).map((name) => expect(adminPage.getByRole('region', { name })).toBeVisible())); }); }); @@ -107,7 +107,7 @@ test.describe.serial('homepage', () => { }); test.describe('for regular users', () => { - const notVisibleCards = [CardIds.Users, CardIds.Custom]; + const notVisibleCards = [CardNames.Users, CardNames.Custom]; test.beforeAll(async ({ api, browser }) => { expect((await api.post('/settings/Layout_Home_Body', { value: '' })).status()).toBe(200); @@ -126,14 +126,14 @@ test.describe.serial('homepage', () => { }); await test.step(`expect ${notVisibleCards.join(' and ')} cards to not be visible`, async () => { - await Promise.all(notVisibleCards.map((id) => expect(regularUserPage.locator(`[data-qa-id="${id}"]`)).not.toBeVisible())); + await Promise.all(notVisibleCards.map((name) => expect(regularUserPage.getByRole('region', { name })).not.toBeVisible())); }); await test.step('expect all other cards to be visible', async () => { await Promise.all( - Object.values(CardIds) - .filter((id) => !notVisibleCards.includes(id)) - .map((id) => expect(regularUserPage.locator(`[data-qa-id="${id}"]`)).toBeVisible()), + Object.values(CardNames) + .filter((name) => !notVisibleCards.includes(name)) + .map((name) => expect(regularUserPage.getByRole('region', { name })).toBeVisible()), ); }); diff --git a/apps/meteor/tests/e2e/settings-int.spec.ts b/apps/meteor/tests/e2e/settings-int.spec.ts index b7ac6c4c25eab..a7b8124a7832c 100644 --- a/apps/meteor/tests/e2e/settings-int.spec.ts +++ b/apps/meteor/tests/e2e/settings-int.spec.ts @@ -14,7 +14,6 @@ test.describe.serial('settings-int', () => { await pageTitle.waitFor(); await expect(pageTitle).toBeVisible(); - await expect(pageTitle).toHaveText('Message'); }); test('expect not being able to set int value as empty string', async ({ page }) => { diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ebde7631d98ce..3debdaebe2027 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -1626,6 +1626,7 @@ "Custom_User_Status_Info": "Custom User Status Info", "Custom_User_Status_Updated_Successfully": "Custom User Status Updated Successfully", "Custom_agent": "Custom agent", + "Custom_content": "Custom content", "Custom_dates": "Custom Dates", "Custom_oauth_helper": "When setting up your OAuth Provider, you'll have to inform a Callback URL. Use
%s
.", "Custom_roles": "Custom roles", From 385f795cbfcc45fbb9e5da81ced6bd7465049549 Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Wed, 31 Dec 2025 17:09:01 -0300 Subject: [PATCH 017/104] fix: CI for external PRs (#38030) --- .github/actions/build-docker/action.yml | 52 +++++++++++++++----- .github/workflows/ci-test-e2e.yml | 38 ++++++++++----- .github/workflows/ci.yml | 63 ++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 30 deletions(-) diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 5100528ac7ae0..12bd993c665da 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -119,25 +119,53 @@ runs: echo "Contents of /tmp/meta.json:" cat /tmp/meta.json - SERVICE_SUFFIX=${{ inputs.service == 'rocketchat' && inputs.type == 'coverage' && (github.event_name == 'release' || github.ref == 'refs/heads/develop') && '-cov' || '' }} + if [[ "${{ inputs.publish-image }}" == 'true' ]]; then + SERVICE_SUFFIX=${{ inputs.service == 'rocketchat' && inputs.type == 'coverage' && (github.event_name == 'release' || github.ref == 'refs/heads/develop') && '-cov' || '' }} + + mkdir -p /tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }} - mkdir -p /tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }} + # Get digest and image info + DIGEST=$(jq -r '.["${{ inputs.service }}"].["containerimage.digest"]' "/tmp/meta.json") + IMAGE_NO_TAG=$(echo "$IMAGE" | sed 's/:.*$//') + FULL_IMAGE="${IMAGE_NO_TAG}@${DIGEST}" - # Get digest and image info - DIGEST=$(jq -r '.["${{ inputs.service }}"].["containerimage.digest"]' "/tmp/meta.json") - IMAGE_NO_TAG=$(echo "$IMAGE" | sed 's/:.*$//') - FULL_IMAGE="${IMAGE_NO_TAG}@${DIGEST}" + echo "Inspecting image: $FULL_IMAGE" - echo "Inspecting image: $FULL_IMAGE" + # Inspect the image and save complete manifest with sizes (using -v for verbose) + docker manifest inspect -v "$FULL_IMAGE" > "/tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" + + echo "Saved manifest to /tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" + cat "/tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" | jq '.' + fi - # Inspect the image and save complete manifest with sizes (using -v for verbose) - docker manifest inspect -v "$FULL_IMAGE" > "/tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" + - name: Save Docker image as artifact + if: inputs.publish-image == 'false' && inputs.arch == 'amd64' + shell: bash + run: | + set -o xtrace - echo "Saved manifest to /tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" - cat "/tmp/manifests/${{ inputs.service }}${SERVICE_SUFFIX}/${{ inputs.arch }}/manifest.json" | jq '.' + # Get image name from docker-compose-ci.yml + IMAGE=$(docker compose -f docker-compose-ci.yml config --format json 2>/dev/null | jq -r --arg s "${{ inputs.service }}" '.services[$s].image') + + # Create directory for image archives + mkdir -p /tmp/docker-images + + # Save the image to a tar file + docker save "${IMAGE}" -o "/tmp/docker-images/${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }}.tar" + + echo "Saved image to /tmp/docker-images/${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }}.tar" + ls -lh /tmp/docker-images/ + + - name: Upload Docker image artifact + if: inputs.publish-image == 'false' && inputs.arch == 'amd64' + uses: actions/upload-artifact@v4 + with: + name: docker-image-${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }} + path: /tmp/docker-images/${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }}.tar + retention-days: 1 - uses: actions/upload-artifact@v4 - if: inputs.publish-image == 'true' + if: inputs.publish-image == 'true' && inputs.arch == 'amd64' with: name: manifests-${{ inputs.service }}-${{ inputs.arch }}-${{ inputs.type }} path: /tmp/manifests diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 7b314ada7c290..0e41352406b56 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -64,7 +64,7 @@ env: MONGO_URL: mongodb://localhost:27017/rocketchat?replicaSet=rs0&directConnection=true TOOL_NODE_FLAGS: ${{ vars.TOOL_NODE_FLAGS }} LOWERCASE_REPOSITORY: ${{ inputs.lowercase-repo }} - DOCKER_TAG: ${{ inputs.gh-docker-tag }} + DOCKER_TAG: ${{ inputs.gh-docker-tag }}-amd64 jobs: test: @@ -135,17 +135,33 @@ jobs: run: | tar -xzf /tmp/RocketChat-packages-build.tar.gz -C . - # if we are testing a PR from a fork, we need to build the docker image at this point - - uses: ./.github/actions/build-docker - if: github.event_name == 'pull_request' && (github.event.pull_request.head.repo.full_name != github.repository || github.actor == 'dependabot[bot]') + # Download Docker images from build artifacts + - name: Download Docker images + uses: actions/download-artifact@v7 + if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' with: - CR_USER: ${{ secrets.CR_USER }} - CR_PAT: ${{ secrets.CR_PAT }} - # the same reason we need to rebuild the docker image at this point is the reason we dont want to publish it - publish-image: false - arch: amd64 - service: 'rocketchat' - type: 'coverage' + pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || 'docker-image-*-amd64-coverage' }} + path: /tmp/docker-images + merge-multiple: true + + # Load Docker images + - name: Load Docker images + if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' + shell: bash + run: | + set -o xtrace + + # Load all downloaded images + for image_file in /tmp/docker-images/*.tar; do + if [ -f "$image_file" ]; then + echo "Loading image from $image_file" + docker load -i "$image_file" + rm "$image_file" + fi + done + + # List loaded images + docker images - name: Set DEBUG_LOG_LEVEL (debug enabled) if: runner.debug == '1' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05c6a858b35d9..3c36030e2b19b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -314,7 +314,7 @@ jobs: # we only build and publish the actual docker images if not a PR from a fork - name: Image ${{ matrix.service[0] }} uses: ./.github/actions/build-docker - if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' + if: github.actor != 'dependabot[bot]' env: # add suffix for the extra images with coverage if building for production DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && (github.event_name == 'release' || github.ref == 'refs/heads/develop') && '-cov' || '' }} @@ -325,10 +325,11 @@ jobs: arch: ${{ matrix.arch }} service: ${{ matrix.service[0] }} type: ${{ matrix.type }} + publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }} - name: Image ${{ matrix.service[1] || '"skipped"' }} uses: ./.github/actions/build-docker - if: matrix.service[1] && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' + if: matrix.service[1] && github.actor != 'dependabot[bot]' env: DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && '-cov' || '' }} with: @@ -338,11 +339,12 @@ jobs: arch: ${{ matrix.arch }} service: ${{ matrix.service[1] }} type: ${{ matrix.type }} + publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }} setup-docker: false - name: Image ${{ matrix.service[2] || '"skipped"' }} uses: ./.github/actions/build-docker - if: matrix.service[2] && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' + if: matrix.service[2] && github.actor != 'dependabot[bot]' env: DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && '-cov' || '' }} with: @@ -352,11 +354,12 @@ jobs: arch: ${{ matrix.arch }} service: ${{ matrix.service[2] }} type: ${{ matrix.type }} + publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }} setup-docker: false - name: Image ${{ matrix.service[3] || '"skipped"' }} uses: ./.github/actions/build-docker - if: matrix.service[3] && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') && github.actor != 'dependabot[bot]' + if: matrix.service[3] && github.actor != 'dependabot[bot]' env: DOCKER_TAG_SUFFIX_ROCKETCHAT: ${{ matrix.type == 'coverage' && '-cov' || '' }} with: @@ -366,6 +369,7 @@ jobs: arch: ${{ matrix.arch }} service: ${{ matrix.service[3] }} type: ${{ matrix.type }} + publish-image: ${{ github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop' }} setup-docker: false build-gh-docker-publish: @@ -452,6 +456,7 @@ jobs: - name: Track Docker image sizes uses: ./.github/actions/docker-image-size-tracker + if: github.actor != 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository with: github-token: ${{ secrets.GITHUB_TOKEN }} ci-pat: ${{ secrets.CI_PAT }} @@ -580,10 +585,11 @@ jobs: REPORTER_ROCKETCHAT_URL: ${{ secrets.REPORTER_ROCKETCHAT_URL }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} REPORTER_JIRA_ROCKETCHAT_API_KEY: ${{ secrets.REPORTER_JIRA_ROCKETCHAT_API_KEY }} + test-federation-matrix: name: 🔨 Test Federation Matrix needs: [checks, build-gh-docker-publish, packages-build, release-versions] - runs-on: ubuntu-24.04-arm + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 @@ -621,15 +627,58 @@ jobs: sudo -- sh -c "echo '127.0.0.1 hs1' >> /etc/hosts" sudo -- sh -c "echo '127.0.0.1 rc1' >> /etc/hosts" + # Download Docker images from build artifacts + - name: Download Docker images + uses: actions/download-artifact@v7 + if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' + with: + pattern: 'docker-image-rocketchat-amd64-coverage' + path: /tmp/docker-images + merge-multiple: true + + # Load Docker images + - name: Load Docker images + if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' + shell: bash + run: | + set -o xtrace + + # Load all downloaded images + for image_file in /tmp/docker-images/*.tar; do + if [ -f "$image_file" ]; then + echo "Loading image from $image_file" + docker load -i "$image_file" + rm "$image_file" + fi + done + + # List loaded images + docker images + - name: Run federation integration tests with pre-built image working-directory: ./ee/packages/federation-matrix env: - ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }} - ENTERPRISE_LICENSE_RC1: ${{ secrets.ENTERPRISE_LICENSE_RC1 }} + ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}-amd64 + ENTERPRISE_LICENSE_RC1: ZEuDWcAxkdBZ0iOzn+JIi7Ri0GKPR43hTueeqEEeTjJhzhp1jM7+fA9LiT3aCzU/oJwudwWLFAwqjrtR13axza+Us6lHuAMdfut/1Z6upRWdSgose1LfDP9Nzce6xOVbO3InQonwTQVQJotlYEGRjiry7jn68TSIKhmjMgC6SVYt6v+syEKRgj+r2oT0xNkurQYGGG1AIYHDqGWa1cX0FVd1ddOKU/DNuCJQxH8Rz5aJC2grIKMIzmRVHfBDJAipeTDl6VI28VM5ExEl3w8zDlUk8wCxXawXGCht0A7jZGCd4IQLDNZs/3Zv+nHC4lcDVzjDu+o17vUIEad4m+nhZgGTNlHqkrH3cqEEEPa3bSh8GKBzLmKHB+i0H3dweT9iqGwz56Nue7twyt5yuGq6qYdtrEx0pEKjystU15DUiQxDPqkBL8yRkp5WScsvJIlhiY+4tU6yKI/GAYtU0g+fCYzjzwxXc7tLg5NeY9kiRMdQ+jRytl3ztHGiv5ERhjQKT9ZpUWiCSCmdr8L3njfLLW1e5/AKmXpg00D6HfJvI30xDcoJwmWnCzFvd7KlSbVwNVBlD6KE9+0j6GV1h0JEml1YrpXUxbpEBz5ALdLn2iVPQ3MT5RODRI5yffSX9ikFkwcH360ewU6Zp63WKRkHyfnzE+tsYe96XdaMZowe7Lw= QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }} PR_NUMBER: ${{ github.event.number }} run: yarn test:integration --image "${ROCKETCHAT_IMAGE}" + - name: Show rc server logs if tests failed + if: failure() + working-directory: ./ee/packages/federation-matrix + run: docker compose -f docker-compose.test.yml logs rc1-prebuilt + + - name: Show hs server logs if tests failed + if: failure() + working-directory: ./ee/packages/federation-matrix + run: docker compose -f docker-compose.test.yml logs hs1 + + - name: Show mongo logs if tests failed + if: failure() + working-directory: ./ee/packages/federation-matrix + run: docker compose -f docker-compose.test.yml logs mongo + report-coverage: name: 📊 Report Coverage runs-on: ubuntu-24.04 From d3511eac8ab0ef739ef4bf0d07e668a7409fa3cc Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:21:51 -0300 Subject: [PATCH 018/104] fix: User roles showing on the message header after opening user card, but with "Show roles" setting/preference disabled (#37984) --- apps/meteor/client/components/message/MessageHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index d3343fa00b3ce..427f61803b584 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -45,7 +45,7 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { const showRoles = useMessageListShowRoles(); const roles = useMessageRoles(message.u._id, message.rid, showRoles); - const shouldShowRolesList = roles.length > 0; + const shouldShowRolesList = showRoles && roles.length > 0; return ( From c6ace77b6a182aeb82646e0c88be1a753e0a04bc Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 6 Jan 2026 19:55:08 -0300 Subject: [PATCH 019/104] fix(CI): expired enterprise license for rc1 domain (#38051) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c36030e2b19b..7d589614476d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -659,7 +659,7 @@ jobs: working-directory: ./ee/packages/federation-matrix env: ROCKETCHAT_IMAGE: ghcr.io/${{ needs.release-versions.outputs.lowercase-repo }}/rocket.chat:${{ needs.release-versions.outputs.gh-docker-tag }}-amd64 - ENTERPRISE_LICENSE_RC1: ZEuDWcAxkdBZ0iOzn+JIi7Ri0GKPR43hTueeqEEeTjJhzhp1jM7+fA9LiT3aCzU/oJwudwWLFAwqjrtR13axza+Us6lHuAMdfut/1Z6upRWdSgose1LfDP9Nzce6xOVbO3InQonwTQVQJotlYEGRjiry7jn68TSIKhmjMgC6SVYt6v+syEKRgj+r2oT0xNkurQYGGG1AIYHDqGWa1cX0FVd1ddOKU/DNuCJQxH8Rz5aJC2grIKMIzmRVHfBDJAipeTDl6VI28VM5ExEl3w8zDlUk8wCxXawXGCht0A7jZGCd4IQLDNZs/3Zv+nHC4lcDVzjDu+o17vUIEad4m+nhZgGTNlHqkrH3cqEEEPa3bSh8GKBzLmKHB+i0H3dweT9iqGwz56Nue7twyt5yuGq6qYdtrEx0pEKjystU15DUiQxDPqkBL8yRkp5WScsvJIlhiY+4tU6yKI/GAYtU0g+fCYzjzwxXc7tLg5NeY9kiRMdQ+jRytl3ztHGiv5ERhjQKT9ZpUWiCSCmdr8L3njfLLW1e5/AKmXpg00D6HfJvI30xDcoJwmWnCzFvd7KlSbVwNVBlD6KE9+0j6GV1h0JEml1YrpXUxbpEBz5ALdLn2iVPQ3MT5RODRI5yffSX9ikFkwcH360ewU6Zp63WKRkHyfnzE+tsYe96XdaMZowe7Lw= + ENTERPRISE_LICENSE_RC1: ZAikY+LLaal7mT6RNYxpyWEmMQyucrl50/7pYBXqHczc90j+RLwF+T0xuCT2pIpKMC5DxcZ1TtkV6MYJk5whrwmap+mQ0FV+VpILJlL0i4T21K4vMfzZXTWm/pzcAy2fMTUNH+mUA9HTBD6lYYh40KnbGXPAd80VbZk0MO/WbWBm2dOT0YCwfvlRyurRqkDAQrftLaffzCNUsMKk0fh+MKs73UDHZQDp1yvs7WoGpPu5ZVi5mTBOt3ZKVz5KjGfClLwJptFPmW1w6nKelAiJBDPpjcX1ylfjxpnBoixko7uN52zlyaeoAYwfRcdDLnZ8k0Ou6tui/vTQUXjGIjHw2AhMaKwonn4E9LYpuA1KEXt08qJL5J3ZtjSCV1T+A9Z3zFhhLgp5dxP/PPUbxDn/P8XKp7nXM9duIfcCMlnea7V8ixEyCHwwvKQaXVVidcsUGtB8CwS0GlsAEBLOzqMehuQUK2rdQ4WgEz3AYveikeVvSzgBHvyXsxssWAThc0Mht0eEJqdDhUB2QeZ2WmPsaSSD639Z4WgjSUoR0zh8bfqepH+2XRcUryXe2yN+iU+3POzi9wfg0k65MxXT8pBg3PD5RHnR8oflEP0tpZts33JiBhYRxX3MKplAFm4dMuphTsDJTh+e534pT7IPuZF79QSVaLEWZfVVVb7nGFtmMwA= QASE_TESTOPS_JEST_API_TOKEN: ${{ secrets.QASE_TESTOPS_JEST_API_TOKEN }} PR_NUMBER: ${{ github.event.number }} run: yarn test:integration --image "${ROCKETCHAT_IMAGE}" From 5ff2596e28689d16c28186608ec9526d33ea7f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Tue, 6 Jan 2026 22:56:11 -0300 Subject: [PATCH 020/104] test: remove `data-qa-setting-id` locators (#38002) --- .../__snapshots__/SettingToggle.spec.tsx.snap | 3 --- .../settings/Setting/inputs/ActionSettingInput.tsx | 4 ++-- .../settings/Setting/inputs/BooleanSettingInput.tsx | 10 ++-------- .../admin/settings/Setting/inputs/CodeSettingInput.tsx | 3 +-- .../settings/Setting/inputs/ColorSettingInput.tsx | 5 +---- .../admin/settings/Setting/inputs/FontSettingInput.tsx | 3 +-- .../settings/Setting/inputs/GenericSettingInput.tsx | 3 +-- .../admin/settings/Setting/inputs/IntSettingInput.tsx | 3 +-- .../settings/Setting/inputs/LanguageSettingInput.tsx | 3 +-- .../settings/Setting/inputs/LookupSettingInput.tsx | 3 +-- .../Setting/inputs/MultiSelectSettingInput.tsx | 4 ++-- .../settings/Setting/inputs/PasswordSettingInput.tsx | 3 +-- .../settings/Setting/inputs/RangeSettingInput.tsx | 3 +-- .../Setting/inputs/RelativeUrlSettingInput.tsx | 3 +-- .../settings/Setting/inputs/RoomPickSettingInput.tsx | 2 +- .../settings/Setting/inputs/SelectSettingInput.tsx | 3 +-- .../Setting/inputs/SelectTimezoneSettingInput.tsx | 3 +-- .../settings/Setting/inputs/StringSettingInput.tsx | 4 +--- .../settings/Setting/inputs/TimespanSettingInput.tsx | 3 +-- .../__snapshots__/RangeSettingInput.spec.tsx.snap | 1 - apps/meteor/tests/e2e/page-objects/admin-settings.ts | 2 +- .../tests/e2e/page-objects/omnichannel-settings.ts | 2 +- .../e2e/settings-persistence-on-ui-navigation.spec.ts | 9 +++++---- 23 files changed, 28 insertions(+), 54 deletions(-) diff --git a/apps/meteor/client/views/admin/ABAC/ABACSettingTab/__snapshots__/SettingToggle.spec.tsx.snap b/apps/meteor/client/views/admin/ABAC/ABACSettingTab/__snapshots__/SettingToggle.spec.tsx.snap index 7c48a94fb6800..78b091fbbf32f 100644 --- a/apps/meteor/client/views/admin/ABAC/ABACSettingTab/__snapshots__/SettingToggle.spec.tsx.snap +++ b/apps/meteor/client/views/admin/ABAC/ABACSettingTab/__snapshots__/SettingToggle.spec.tsx.snap @@ -31,7 +31,6 @@ exports[`AbacEnabledToggle should be disabled when abac license is not installed
diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx index 624150b1709b6..367a8efe5aff7 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx @@ -30,14 +30,8 @@ function BooleanSettingInput({ {label} - {hasResetButton && } - + {hasResetButton && } + {hint && {hint}} diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx index 4302ad165000e..bed4565cf80aa 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/CodeSettingInput.tsx @@ -38,12 +38,11 @@ function CodeSettingInput({ {label} - {hasResetButton && } + {hasResetButton && } {hint && {hint}} {label} - {hasResetButton && } + {hasResetButton && } @@ -61,7 +61,6 @@ function ColorSettingInput({ {editor === 'color' && ( {label} - {hasResetButton && } + {hasResetButton && } {label} - {hasResetButton && } + {hasResetButton && } + +
+
+ + + + +
+ + + + + + + + + +`; From 0b438a5792e672d2858ce02a3b78ad16d8b552ff Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 19 Jan 2026 14:17:51 -0600 Subject: [PATCH 085/104] chore: Adapt logs to new format (#38259) --- .../functions/getWorkspaceAccessToken.ts | 12 ++++++--- .../supportedVersionsToken.ts | 10 +++++-- .../server/startup/custom-sounds.js | 5 +++- .../server/startup/emoji-custom.js | 5 +++- .../server/config/_configUploadStorage.ts | 5 +++- .../app/file-upload/server/lib/FileUpload.ts | 9 +++++-- .../app/lib/server/functions/setUserAvatar.ts | 27 ++++++++++++++----- .../app/livechat/server/sendMessageBySMS.ts | 6 ++++- .../server/functions/sendMail.ts | 10 +++++-- .../server/unreadMessages.ts | 5 +++- .../meteor-accounts-saml/server/lib/SAML.ts | 5 +++- .../server/lib/settings.ts | 5 +++- .../meteor-accounts-saml/server/listener.ts | 7 +++-- apps/meteor/app/push/server/apn.ts | 16 ++++++++--- apps/meteor/app/push/server/push.ts | 21 ++++++++++++--- 15 files changed, 116 insertions(+), 32 deletions(-) diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index 6595c8e90fc48..3545e734004ca 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -34,13 +34,17 @@ export async function getWorkspaceAccessToken(forceNew = false, scope = '', save const workspaceCredentials = await WorkspaceCredentials.getCredentialByScope(scope); if (workspaceCredentials && !hasWorkspaceAccessTokenExpired(workspaceCredentials) && !forceNew) { - SystemLogger.debug( - `Workspace credentials cache hit using scope: ${scope}. Avoiding generating a new access token from cloud services.`, - ); + SystemLogger.debug({ + msg: 'Workspace credentials cache hit. Avoiding generating a new access token from cloud services.', + scope, + }); return workspaceCredentials.accessToken; } - SystemLogger.debug(`Workspace credentials cache miss using scope: ${scope}, fetching new access token from cloud services.`); + SystemLogger.debug({ + msg: 'Workspace credentials cache miss, fetching new access token from cloud services.', + scope, + }); const accessToken = await getWorkspaceAccessTokenWithScope({ scope, throwOnError }); diff --git a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts index b14d3a77ec4a0..2643b673c5b24 100644 --- a/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts +++ b/apps/meteor/app/cloud/server/functions/supportedVersionsToken/supportedVersionsToken.ts @@ -64,7 +64,10 @@ const cacheValueInSettings = ( reset: (retry?: number) => Promise; } => { const reset = async (retry?: number) => { - SystemLogger.debug(`Resetting cached value ${key} in settings`); + SystemLogger.debug({ + msg: 'Resetting cached value in settings', + key, + }); const value = await fn(retry); if ( @@ -181,7 +184,10 @@ const getSupportedVersionsToken = async (retry = 0) => { 5000 * Math.pow(2, retry), ); } else { - SystemLogger.error(`Failed to get supported versions from cloud after ${retry} retries.`); + SystemLogger.error({ + msg: 'Failed to get supported versions from cloud after retries.', + retry, + }); await buildVersionUpdateMessage(supportedVersions?.versions); } diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js index 2f4f77aa0bd2d..117a7d3c9e759 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js @@ -20,7 +20,10 @@ Meteor.startup(() => { throw new Error(`Invalid RocketChatStore type [${storeType}]`); } - SystemLogger.info(`Using ${storeType} for custom sounds storage`); + SystemLogger.info({ + msg: 'Using custom sounds storage', + storeType, + }); let path = '~/uploads'; if (settings.get('CustomSounds_FileSystemPath') != null) { diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index 28e82cf0849ed..fed5123b115f4 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -21,7 +21,10 @@ Meteor.startup(() => { throw new Error(`Invalid RocketChatStore type [${storeType}]`); } - SystemLogger.info(`Using ${storeType} for custom emoji storage`); + SystemLogger.info({ + msg: 'Using custom emoji storage', + storeType, + }); let path = '~/uploads'; if (settings.get('EmojiUpload_FileSystemPath') != null) { diff --git a/apps/meteor/app/file-upload/server/config/_configUploadStorage.ts b/apps/meteor/app/file-upload/server/config/_configUploadStorage.ts index 55cc1afbbab16..8629304cb2984 100644 --- a/apps/meteor/app/file-upload/server/config/_configUploadStorage.ts +++ b/apps/meteor/app/file-upload/server/config/_configUploadStorage.ts @@ -13,7 +13,10 @@ const configStore = _.debounce(() => { const store = settings.get('FileUpload_Storage_Type'); if (store) { - SystemLogger.info(`Setting default file store to ${store}`); + SystemLogger.info({ + msg: 'Setting default file store', + store, + }); UploadFS.getStores().Avatars = UploadFS.getStore(`${store}:Avatars`); UploadFS.getStores().Uploads = UploadFS.getStore(`${store}:Uploads`); UploadFS.getStores().UserDataFiles = UploadFS.getStore(`${store}:UserDataFiles`); diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.ts b/apps/meteor/app/file-upload/server/lib/FileUpload.ts index f7a8890e5bf58..8c1aa7b9d0e3e 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.ts +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.ts @@ -537,12 +537,17 @@ export const FileUpload = { getStoreByName(handlerName?: string) { if (!handlerName) { - SystemLogger.error(`Empty Upload handler does not exists`); + SystemLogger.error({ + msg: 'Empty Upload handler does not exists', + }); throw new Error(`Empty Upload handler does not exists`); } if (this.handlers[handlerName] == null) { - SystemLogger.error(`Upload handler "${handlerName}" does not exists`); + SystemLogger.error({ + msg: 'Upload handler does not exists', + handlerName, + }); } return this.handlers[handlerName]; }, diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts index e5a12b110e2b9..85e12ef044f44 100644 --- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts @@ -114,7 +114,11 @@ export async function setUserAvatar( try { response = await fetch(dataURI, { redirect: 'error' }); } catch (e) { - SystemLogger.info(`Not a valid response, from the avatar url: ${encodeURI(dataURI)}`); + SystemLogger.info({ + msg: 'Not a valid response from the avatar url', + url: encodeURI(dataURI), + err: e, + }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${encodeURI(dataURI)}`, { function: 'setUserAvatar', url: dataURI, @@ -123,7 +127,12 @@ export async function setUserAvatar( if (response.status !== 200) { if (response.status !== 404) { - SystemLogger.info(`Error while handling the setting of the avatar from a url (${encodeURI(dataURI)}) for ${user.username}`); + SystemLogger.info({ + msg: 'Error while handling the setting of the avatar from a url', + url: encodeURI(dataURI), + username: user.username, + status: response.status, + }); throw new Meteor.Error( 'error-avatar-url-handling', `Error while handling avatar setting from a URL (${encodeURI(dataURI)}) for ${user.username}`, @@ -131,7 +140,11 @@ export async function setUserAvatar( ); } - SystemLogger.info(`Not a valid response, ${response.status}, from the avatar url: ${dataURI}`); + SystemLogger.info({ + msg: 'Not a valid response from the avatar url', + status: response.status, + url: dataURI, + }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, @@ -139,9 +152,11 @@ export async function setUserAvatar( } if (!/image\/.+/.test(response.headers.get('content-type') || '')) { - SystemLogger.info( - `Not a valid content-type from the provided url, ${response.headers.get('content-type')}, from the avatar url: ${dataURI}`, - ); + SystemLogger.info({ + msg: 'Not a valid content-type from the provided avatar url', + contentType: response.headers.get('content-type'), + url: dataURI, + }); throw new Meteor.Error('error-avatar-invalid-url', `Invalid avatar URL: ${dataURI}`, { function: 'setUserAvatar', url: dataURI, diff --git a/apps/meteor/app/livechat/server/sendMessageBySMS.ts b/apps/meteor/app/livechat/server/sendMessageBySMS.ts index 5b79806951ca1..2d370410fa337 100644 --- a/apps/meteor/app/livechat/server/sendMessageBySMS.ts +++ b/apps/meteor/app/livechat/server/sendMessageBySMS.ts @@ -63,7 +63,11 @@ callbacks.add( try { await SMSService.send(room.sms.from, visitor.phone[0].phoneNumber, message.msg, extraData); - callbackLogger.debug(`SMS message sent to ${visitor.phone[0].phoneNumber} via ${service}`); + callbackLogger.debug({ + msg: 'SMS message sent', + phoneNumber: visitor.phone[0].phoneNumber, + service, + }); } catch (e) { callbackLogger.error(e); } diff --git a/apps/meteor/app/mail-messages/server/functions/sendMail.ts b/apps/meteor/app/mail-messages/server/functions/sendMail.ts index 50435bc248129..c0e179d919d0a 100644 --- a/apps/meteor/app/mail-messages/server/functions/sendMail.ts +++ b/apps/meteor/app/mail-messages/server/functions/sendMail.ts @@ -57,7 +57,10 @@ export const sendMail = async function ({ email, }); - SystemLogger.debug(`Sending email to ${email}`); + SystemLogger.debug({ + msg: 'Sending email', + email, + }); return Mailer.send({ to: email, from, @@ -83,7 +86,10 @@ export const sendMail = async function ({ name: escapeHTML(user.name || ''), email: escapeHTML(email), }); - SystemLogger.debug(`Sending email to ${email}`); + SystemLogger.debug({ + msg: 'Sending email', + email, + }); await Mailer.send({ to: email, from, diff --git a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts index 202dde93d062d..ccf972c561e31 100644 --- a/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts +++ b/apps/meteor/app/message-mark-as-unread/server/unreadMessages.ts @@ -69,7 +69,10 @@ export const unreadMessages = async (userId: string, firstUnreadMessage?: Pick role._id); if (roles.length !== normalizedRoleNamesOrIds.length) { - SystemLogger.warn(`Failed to convert some role names to ids: ${normalizedRoleNamesOrIds.join(', ')}`); + SystemLogger.warn({ + msg: 'Failed to convert some role names to ids', + roles: normalizedRoleNamesOrIds, + }); } if (!roles.length) { diff --git a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts index 917032c54306e..3b93fe22c88b4 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/lib/settings.ts @@ -151,7 +151,10 @@ export const loadSamlServiceProviders = async function (): Promise { }; export const addSamlService = function (name: string): void { - SystemLogger.warn(`Adding ${name} is deprecated`); + SystemLogger.warn({ + msg: 'Adding SAML service is deprecated', + serviceName: name, + }); }; export const addSettings = async function (name: string): Promise { diff --git a/apps/meteor/app/meteor-accounts-saml/server/listener.ts b/apps/meteor/app/meteor-accounts-saml/server/listener.ts index 8cdf7c9e6f636..9af8c4d84c2d1 100644 --- a/apps/meteor/app/meteor-accounts-saml/server/listener.ts +++ b/apps/meteor/app/meteor-accounts-saml/server/listener.ts @@ -54,14 +54,17 @@ const middleware = async function (req: express.Request, res: ServerResponse, ne const service = SAMLUtils.getServiceProviderOptions(samlObject.serviceName); if (!service) { - SystemLogger.error(`${samlObject.serviceName} service provider not found`); + SystemLogger.error({ + msg: 'SAML service provider not found', + serviceName: samlObject.serviceName, + }); throw new Error('SAML Service Provider not found.'); } await SAML.processRequest(req, res, service, samlObject); } catch (err) { // @ToDo: Ideally we should send some error message to the client, but there's no way to do it on a redirect right now. - SystemLogger.error(err); + SystemLogger.error({ err }); const url = Meteor.absoluteUrl('home'); res.writeHead(302, { diff --git a/apps/meteor/app/push/server/apn.ts b/apps/meteor/app/push/server/apn.ts index cd335aa06bad6..634665cad9403 100644 --- a/apps/meteor/app/push/server/apn.ts +++ b/apps/meteor/app/push/server/apn.ts @@ -70,10 +70,17 @@ export const sendAPN = ({ void apnConnection.send(note, userToken).then((response) => { response.failed.forEach((failure) => { - logger.debug(`Got error code ${failure.status} for token ${userToken}`); + logger.debug({ + msg: 'Got error code for APN token', + status: failure.status, + token: userToken, + }); if (['400', '410'].includes(String(failure.status))) { - logger.debug(`Removing token ${userToken}`); + logger.debug({ + msg: 'Removing APN token', + token: userToken, + }); _removeToken({ apn: userToken, }); @@ -105,7 +112,10 @@ export const initAPN = ({ options, absoluteUrl }: { options: RequiredField`, err: response }); + logger.error({ msg: 'Error sending push to gateway', tries, err: response }); if (tries <= 4) { // [1, 2, 4, 8, 16] minutes (total 31) @@ -368,7 +368,11 @@ class PushClass { throw new Error('Push.send: option "text" not a string'); } - logger.debug(`send message "${notification.title}" to userId`, notification.userId); + logger.debug({ + msg: 'send message to userId', + title: notification.title, + userId: notification.userId, + }); const query = { userId: notification.userId, @@ -389,7 +393,12 @@ class PushClass { } if (settings.get('Log_Level') === '2') { - logger.debug(`Sent message "${notification.title}" to ${countApn.length} ios apps ${countGcm.length} android apps`); + logger.debug({ + msg: 'Sent message to apps', + title: notification.title, + iosApps: countApn.length, + androidApps: countGcm.length, + }); // Add some verbosity about the send result, making sure the developer // understands what just happened. @@ -489,7 +498,11 @@ class PushClass { try { await this.sendNotification(notification); } catch (error: any) { - logger.debug(`Could not send notification to user "${notification.userId}", Error: ${error.message}`); + logger.debug({ + msg: 'Could not send notification to user', + userId: notification.userId, + err: error, + }); logger.debug(error.stack); } } From 1e0b37f68e1622a34879c1ccf7c77c07b373e00c Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 19 Jan 2026 18:24:15 -0300 Subject: [PATCH 086/104] feat: Realtime user presence updates when starting/transfering a voice call (#37616) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/purple-jobs-swim.md | 5 + .../ui-voip/src/context/MediaCallContext.ts | 67 +--- packages/ui-voip/src/context/index.ts | 5 +- .../src/context/usePeerAutocomplete.spec.tsx | 328 ++++++++++++++++++ .../src/context/usePeerAutocomplete.ts | 89 +++++ packages/ui-voip/src/utils/queryKeys.ts | 4 + 6 files changed, 430 insertions(+), 68 deletions(-) create mode 100644 .changeset/purple-jobs-swim.md create mode 100644 packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx create mode 100644 packages/ui-voip/src/context/usePeerAutocomplete.ts create mode 100644 packages/ui-voip/src/utils/queryKeys.ts diff --git a/.changeset/purple-jobs-swim.md b/.changeset/purple-jobs-swim.md new file mode 100644 index 0000000000000..f95ac7f889285 --- /dev/null +++ b/.changeset/purple-jobs-swim.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/ui-voip": minor +--- + +Introduces realtime user presence updates for the selected user when starting/transferring a voice call. diff --git a/packages/ui-voip/src/context/MediaCallContext.ts b/packages/ui-voip/src/context/MediaCallContext.ts index 463bd335df102..9c8a9d93db14e 100644 --- a/packages/ui-voip/src/context/MediaCallContext.ts +++ b/packages/ui-voip/src/context/MediaCallContext.ts @@ -1,8 +1,6 @@ import type { UserStatus } from '@rocket.chat/core-typings'; -import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import type { Device } from '@rocket.chat/ui-contexts'; -import { keepPreviousData, useQuery } from '@tanstack/react-query'; -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext } from 'react'; import type { PeerAutocompleteOptions } from '../components'; @@ -151,67 +149,4 @@ export const useMediaCallExternalContext = (): return { state: context.state, onToggleWidget: context.onToggleWidget, onEndCall: context.onEndCall, peerInfo: context.peerInfo }; }; -const PREFIX_FIRST_OPTION = 'rcx-first-option-'; - -export const isFirstPeerAutocompleteOption = (value: string) => { - return value.startsWith(PREFIX_FIRST_OPTION); -}; - -const getFirstOption = (filter: string): PeerAutocompleteOptions => { - return { value: `${PREFIX_FIRST_OPTION}${filter}`, label: filter, avatarUrl: '' }; -}; - -export const usePeerAutocomplete = (onSelectPeer: (peerInfo: PeerInfo) => void, peerInfo: PeerInfo | undefined) => { - const { getAutocompleteOptions } = useMediaCallContext(); - const [filter, setFilter] = useState(''); - - const debouncedFilter = useDebouncedValue(filter, 400); - - const { data: options } = useQuery({ - queryKey: ['mediaCall/peerAutocomplete', debouncedFilter], - queryFn: async () => { - const options = await getAutocompleteOptions(debouncedFilter); - - if (debouncedFilter.length > 0) { - return [getFirstOption(debouncedFilter), ...options]; - } - - return options; - }, - placeholderData: keepPreviousData, - initialData: [], - }); - - return { - options, - onChangeFilter: setFilter, - onChangeValue: (value: string | string[]) => { - if (Array.isArray(value)) { - return; - } - - if (isFirstPeerAutocompleteOption(value)) { - onSelectPeer({ number: value.replace(PREFIX_FIRST_OPTION, '') }); - return; - } - - const localInfo = options.find((option) => option.value === value); - - if (!localInfo) { - throw new Error(`Peer info not found for value: ${value}`); - } - - onSelectPeer({ - userId: localInfo.value, - displayName: localInfo.label, - avatarUrl: localInfo.avatarUrl, - status: localInfo.status as UserStatus, - }); - }, - value: peerInfo && 'userId' in peerInfo ? peerInfo.userId : undefined, - filter, - onKeypadPress: (key: string) => setFilter((filter) => filter + key), - }; -}; - export default MediaCallContext; diff --git a/packages/ui-voip/src/context/index.ts b/packages/ui-voip/src/context/index.ts index 8b1ca3d329105..8e187cb42cd72 100644 --- a/packages/ui-voip/src/context/index.ts +++ b/packages/ui-voip/src/context/index.ts @@ -1,4 +1,5 @@ -export { useMediaCallContext, useMediaCallExternalContext, default as MediaCallContext, usePeerAutocomplete } from './MediaCallContext'; +export { useMediaCallContext, useMediaCallExternalContext, default as MediaCallContext } from './MediaCallContext'; export type { PeerInfo, ConnectionState, MediaCallExternalState as MediaCallState } from './MediaCallContext'; -export { isFirstPeerAutocompleteOption, isCallingBlocked } from './MediaCallContext'; +export { isCallingBlocked } from './MediaCallContext'; +export { usePeerAutocomplete, isFirstPeerAutocompleteOption } from './usePeerAutocomplete'; export { default as MockedMediaCallProvider } from './MockedMediaCallProvider'; diff --git a/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx b/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx new file mode 100644 index 0000000000000..c59d5630fa1de --- /dev/null +++ b/packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx @@ -0,0 +1,328 @@ +import { UserStatus } from '@rocket.chat/core-typings'; +import { mockAppRoot } from '@rocket.chat/mock-providers'; +import { renderHook, waitFor, act } from '@testing-library/react'; + +import type { PeerInfo } from './MediaCallContext'; +import type { PeerAutocompleteOptions } from '../components'; +import MediaCallContext, { defaultMediaCallContextValue } from './MediaCallContext'; +import { usePeerAutocomplete, isFirstPeerAutocompleteOption } from './usePeerAutocomplete'; + +jest.mock('@rocket.chat/ui-contexts', () => ({ + ...jest.requireActual('@rocket.chat/ui-contexts'), + useUserPresence: jest.fn(() => ({ _id: 'user1', status: 'online' as const })), +})); + +jest.useFakeTimers(); + +const mockGetAutocompleteOptions = jest.fn(); +const mockOnSelectPeer = jest.fn(); + +const appRoot = () => + mockAppRoot() + .wrap((children) => ( + + {children} + + )) + .build(); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('isFirstPeerAutocompleteOption', () => { + it('should return true for values starting with prefix', () => { + expect(isFirstPeerAutocompleteOption('rcx-first-option-test')).toBe(true); + }); + + it('should return false for values not starting with prefix', () => { + expect(isFirstPeerAutocompleteOption('test')).toBe(false); + }); +}); + +describe('hook', () => { + it('should initialize with empty filter and no value', () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + expect(result.current.filter).toBe(''); + expect(result.current.value).toBeUndefined(); + }); + + it('should update filter when onChangeFilter is called', async () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + act(() => { + result.current.onChangeFilter('test'); + }); + + expect(result.current.filter).toBe('test'); + }); + + it('should fetch autocomplete options', async () => { + const mockOptions: PeerAutocompleteOptions[] = [ + { value: 'user1', label: 'User 1', avatarUrl: '' }, + { value: 'user2', label: 'User 2', avatarUrl: '' }, + ]; + mockGetAutocompleteOptions.mockResolvedValue(mockOptions); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(result.current.options).toEqual(mockOptions); + }); + }); + + it('should add first option when filter is not empty', async () => { + const mockOptions: PeerAutocompleteOptions[] = [{ value: 'user1', label: 'User 1', avatarUrl: '' }]; + mockGetAutocompleteOptions.mockResolvedValue(mockOptions); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + act(() => { + result.current.onChangeFilter('123'); + }); + + await waitFor(() => { + expect(result.current.options).toHaveLength(2); + expect(result.current.options[0]).toEqual({ + value: 'rcx-first-option-123', + label: '123', + avatarUrl: '', + }); + }); + }); + + it('should return value when peerInfo has userId', () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { userId: 'user1', displayName: 'User 1' }; + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + expect(result.current.value).toBe('user1'); + }); + + it('should return undefined value when peerInfo has no userId', () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { number: '123456' }; + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + expect(result.current.value).toBeUndefined(); + }); + + describe('onChangeValue', () => { + it('should do nothing if value is an array', async () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + act(() => { + result.current.onChangeValue(['value1', 'value2']); + }); + + expect(mockOnSelectPeer).not.toHaveBeenCalled(); + }); + + it('should call onSelectPeer with number when value is first option', async () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + act(() => { + result.current.onChangeValue('rcx-first-option-123456'); + }); + + expect(mockOnSelectPeer).toHaveBeenCalledWith({ number: '123456' }); + }); + + it('should call onSelectPeer with full peer info when value matches option', async () => { + const mockOptions: PeerAutocompleteOptions[] = [ + { value: 'user1', label: 'User 1', avatarUrl: 'avatar.jpg', status: UserStatus.ONLINE }, + ]; + mockGetAutocompleteOptions.mockResolvedValue(mockOptions); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(result.current.options).toHaveLength(1); + }); + + act(() => { + result.current.onChangeValue('user1'); + }); + + expect(mockOnSelectPeer).toHaveBeenCalledWith({ + userId: 'user1', + displayName: 'User 1', + avatarUrl: 'avatar.jpg', + status: UserStatus.ONLINE, + }); + }); + + it('should throw error when value does not match any option', async () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(result.current.options).toEqual([]); + }); + + expect(() => { + act(() => { + result.current.onChangeValue('unknown-user'); + }); + }).toThrow('Peer info not found for value: unknown-user'); + }); + }); + + describe('onKeypadPress', () => { + it('should append key to filter', async () => { + mockGetAutocompleteOptions.mockResolvedValue([]); + + const { result } = renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + act(() => { + result.current.onKeypadPress('1'); + }); + + expect(result.current.filter).toBe('1'); + + act(() => { + result.current.onKeypadPress('2'); + }); + + expect(result.current.filter).toBe('12'); + + act(() => { + result.current.onKeypadPress('3'); + }); + + expect(result.current.filter).toBe('123'); + }); + }); + + describe('user presence updates', () => { + it('should update peer status when user presence changes', async () => { + const { useUserPresence } = await import('@rocket.chat/ui-contexts'); + const mockUseUserPresence = useUserPresence as jest.MockedFunction; + + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { userId: 'user1', displayName: 'User 1', status: UserStatus.ONLINE }; + + mockUseUserPresence.mockReturnValue({ _id: 'user1', status: UserStatus.AWAY, statusText: '' }); + + renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(mockOnSelectPeer).toHaveBeenCalledWith({ + ...peerInfo, + status: 'away', + }); + }); + }); + + it('should not update peer status when status has not changed', async () => { + const { useUserPresence } = await import('@rocket.chat/ui-contexts'); + const mockUseUserPresence = useUserPresence as jest.MockedFunction; + + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { userId: 'user1', displayName: 'User 1', status: UserStatus.ONLINE }; + + mockUseUserPresence.mockReturnValue({ _id: 'user1', status: UserStatus.ONLINE, statusText: '' }); + + renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(mockOnSelectPeer).not.toHaveBeenCalled(); + }); + }); + + it('should not update when peerInfo is undefined', async () => { + const { useUserPresence } = await import('@rocket.chat/ui-contexts'); + const mockUseUserPresence = useUserPresence as jest.MockedFunction; + + mockGetAutocompleteOptions.mockResolvedValue([]); + mockUseUserPresence.mockReturnValue({ _id: 'user1', status: UserStatus.ONLINE, statusText: '' }); + + renderHook(() => usePeerAutocomplete(mockOnSelectPeer, undefined), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(mockOnSelectPeer).not.toHaveBeenCalled(); + }); + }); + + it('should not update when peerInfo has no status property', async () => { + const { useUserPresence } = await import('@rocket.chat/ui-contexts'); + const mockUseUserPresence = useUserPresence as jest.MockedFunction; + + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { number: '123456' }; + + mockUseUserPresence.mockReturnValue({ _id: 'user1', status: UserStatus.ONLINE, statusText: '' }); + + renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(mockOnSelectPeer).not.toHaveBeenCalled(); + }); + }); + + it('should not update when useUserPresence returns no status', async () => { + const { useUserPresence } = await import('@rocket.chat/ui-contexts'); + const mockUseUserPresence = useUserPresence as jest.MockedFunction; + + mockGetAutocompleteOptions.mockResolvedValue([]); + const peerInfo: PeerInfo = { userId: 'user1', displayName: 'User 1', status: UserStatus.ONLINE }; + + mockUseUserPresence.mockReturnValue(undefined); + + renderHook(() => usePeerAutocomplete(mockOnSelectPeer, peerInfo), { + wrapper: appRoot(), + }); + + await waitFor(() => { + expect(mockOnSelectPeer).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/packages/ui-voip/src/context/usePeerAutocomplete.ts b/packages/ui-voip/src/context/usePeerAutocomplete.ts new file mode 100644 index 0000000000000..e0f81fac92789 --- /dev/null +++ b/packages/ui-voip/src/context/usePeerAutocomplete.ts @@ -0,0 +1,89 @@ +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { useUserPresence } from '@rocket.chat/ui-contexts'; +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; + +import type { PeerInfo } from './MediaCallContext'; +import { useMediaCallContext } from './MediaCallContext'; +import type { PeerAutocompleteOptions } from '../components'; +import { mediaCallQueryKeys } from '../utils/queryKeys'; + +const PREFIX_FIRST_OPTION = 'rcx-first-option-'; + +export const isFirstPeerAutocompleteOption = (value: string) => { + return value.startsWith(PREFIX_FIRST_OPTION); +}; + +const getFirstOption = (filter: string): PeerAutocompleteOptions => { + return { value: `${PREFIX_FIRST_OPTION}${filter}`, label: filter, avatarUrl: '' }; +}; + +export const usePeerAutocomplete = (onSelectPeer: (peerInfo: PeerInfo) => void, peerInfo: PeerInfo | undefined) => { + const { getAutocompleteOptions } = useMediaCallContext(); + const [filter, setFilter] = useState(''); + + const debouncedFilter = useDebouncedValue(filter, 400); + + const { data: options } = useQuery({ + queryKey: mediaCallQueryKeys.peerAutocomplete(debouncedFilter), + queryFn: async () => { + const options = await getAutocompleteOptions(debouncedFilter); + + if (debouncedFilter.length > 0) { + return [getFirstOption(debouncedFilter), ...options]; + } + + return options; + }, + placeholderData: keepPreviousData, + initialData: [], + }); + + const status = useUserPresence(peerInfo && 'userId' in peerInfo ? peerInfo.userId : undefined); + + useEffect(() => { + if (!peerInfo || !('status' in peerInfo) || !status?.status) { + return; + } + + if (status.status === peerInfo?.status) { + return; + } + + onSelectPeer({ + ...peerInfo, + status: status.status, + }); + }, [status, peerInfo, onSelectPeer]); + + return { + options, + onChangeFilter: setFilter, + onChangeValue: (value: string | string[]) => { + if (Array.isArray(value)) { + return; + } + + if (isFirstPeerAutocompleteOption(value)) { + onSelectPeer({ number: value.replace(PREFIX_FIRST_OPTION, '') }); + return; + } + + const localInfo = options.find((option) => option.value === value); + + if (!localInfo) { + throw new Error(`Peer info not found for value: ${value}`); + } + + onSelectPeer({ + userId: localInfo.value, + displayName: localInfo.label, + avatarUrl: localInfo.avatarUrl, + status: localInfo.status, + }); + }, + value: peerInfo && 'userId' in peerInfo ? peerInfo.userId : undefined, + filter, + onKeypadPress: (key: string) => setFilter((filter) => filter + key), + }; +}; diff --git a/packages/ui-voip/src/utils/queryKeys.ts b/packages/ui-voip/src/utils/queryKeys.ts new file mode 100644 index 0000000000000..d4a48e0ec8299 --- /dev/null +++ b/packages/ui-voip/src/utils/queryKeys.ts @@ -0,0 +1,4 @@ +export const mediaCallQueryKeys = { + all: ['mediaCall'] as const, + peerAutocomplete: (filter: string) => [...mediaCallQueryKeys.all, 'peerAutocomplete', filter] as const, +}; From 00b36c5a59fb45573f72c5409735238a817ff5ca Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:28:47 -0300 Subject: [PATCH 087/104] feat: Add "Direct Message" button to Media Call widget (#38169) Co-authored-by: Tasso Evangelista <2263066+tassoevan@users.noreply.github.com> --- .changeset/eighty-weeks-rush.md | 7 +++++++ .../client/providers/MediaCallProvider.tsx | 1 + apps/meteor/client/views/room/Room.tsx | 3 +++ .../actions/useUserMediaCallAction.spec.tsx | 3 +++ .../src/components/Widget/WidgetHeader.tsx | 4 +++- .../ui-voip/src/context/MediaCallContext.ts | 17 ++++++++++++++++- .../ui-voip/src/context/MediaCallProvider.tsx | 12 ++++++++++-- .../src/context/MockedMediaCallProvider.tsx | 4 ++++ packages/ui-voip/src/hooks/index.ts | 1 + .../src/hooks/useMediaCallOpenRoomTracker.ts | 17 +++++++++++++++++ packages/ui-voip/src/index.ts | 2 +- .../MediaCallWidget/OngoingCall.stories.tsx | 8 ++++++++ .../src/views/MediaCallWidget/OngoingCall.tsx | 19 +++++++++++++++++-- .../MediaCallWidget.spec.tsx.snap | 12 ++++++------ 14 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 .changeset/eighty-weeks-rush.md create mode 100644 packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.ts diff --git a/.changeset/eighty-weeks-rush.md b/.changeset/eighty-weeks-rush.md new file mode 100644 index 0000000000000..d79aadd40c93a --- /dev/null +++ b/.changeset/eighty-weeks-rush.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/ui-client": patch +"@rocket.chat/ui-voip": patch +--- + +Introduces in the call widget a quick link that redirects to the participant's direct message diff --git a/apps/meteor/client/providers/MediaCallProvider.tsx b/apps/meteor/client/providers/MediaCallProvider.tsx index 68d773d49e776..006b2bb067f22 100644 --- a/apps/meteor/client/providers/MediaCallProvider.tsx +++ b/apps/meteor/client/providers/MediaCallProvider.tsx @@ -17,6 +17,7 @@ const MediaCallProvider = ({ children }: { children: ReactNode }) => { onToggleWidget: undefined, onEndCall: undefined, peerInfo: undefined, + setOpenRoomId: undefined, }), [], ); diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index fed2a9da31e58..cc5a119f61c02 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -1,6 +1,7 @@ import { isInviteSubscription } from '@rocket.chat/core-typings'; import { ContextualbarSkeleton } from '@rocket.chat/ui-client'; import { useSetting, useRoomToolbox, useUserId } from '@rocket.chat/ui-contexts'; +import { useMediaCallOpenRoomTracker } from '@rocket.chat/ui-voip'; import type { ReactElement } from 'react'; import { createElement, lazy, memo, Suspense } from 'react'; import { FocusScope } from 'react-aria'; @@ -34,6 +35,8 @@ const Room = (): ReactElement => { const roomLabel = room.t === 'd' ? t('Conversation_with__roomName__', { roomName: room.name }) : t('Channel__roomName__', { roomName: room.name }); + useMediaCallOpenRoomTracker(room._id); + if (subscription && isInviteSubscription(subscription)) { return ( diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx index 68a16d072ee68..0f7f038df95bf 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useUserMediaCallAction.spec.tsx @@ -46,6 +46,7 @@ describe('useUserMediaCallAction', () => { onToggleWidget: undefined, onEndCall: undefined, peerInfo: undefined, + setOpenRoomId: undefined, }); const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid), { wrapper: mockAppRoot().build() }); @@ -114,6 +115,7 @@ describe('useUserMediaCallAction', () => { onToggleWidget: mockOnToggleWidget, peerInfo: undefined, onEndCall: () => undefined, + setOpenRoomId: () => undefined, }); const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid)); @@ -133,6 +135,7 @@ describe('useUserMediaCallAction', () => { onToggleWidget: jest.fn(), peerInfo: undefined, onEndCall: () => undefined, + setOpenRoomId: () => undefined, }); const { result } = renderHook(() => useUserMediaCallAction(fakeUser, mockRid)); diff --git a/packages/ui-voip/src/components/Widget/WidgetHeader.tsx b/packages/ui-voip/src/components/Widget/WidgetHeader.tsx index bc76e085a77e9..8d635607be9a9 100644 --- a/packages/ui-voip/src/components/Widget/WidgetHeader.tsx +++ b/packages/ui-voip/src/components/Widget/WidgetHeader.tsx @@ -13,7 +13,9 @@ const WidgetHeader = ({ title, children }: WidgetHeaderProps): ReactElement => { {title} - {children} + + {children} +
); }; diff --git a/packages/ui-voip/src/context/MediaCallContext.ts b/packages/ui-voip/src/context/MediaCallContext.ts index 9c8a9d93db14e..b01b346c95b92 100644 --- a/packages/ui-voip/src/context/MediaCallContext.ts +++ b/packages/ui-voip/src/context/MediaCallContext.ts @@ -38,6 +38,8 @@ type MediaCallContextType = { remoteMuted: boolean; remoteHeld: boolean; + onClickDirectMessage?: () => void; + onMute: () => void; onHold: () => void; @@ -54,6 +56,8 @@ type MediaCallContextType = { onSelectPeer: (peerInfo: PeerInfo) => void; + setOpenRoomId: (roomId: string | undefined) => void; + getAutocompleteOptions: (filter: string) => Promise; // This is used to get the peer info from the server in case it's not available in the autocomplete options. getPeerInfo: (id: string) => Promise; @@ -88,6 +92,8 @@ export const defaultMediaCallContextValue: MediaCallContextType = { onSelectPeer: () => undefined, + setOpenRoomId: () => undefined, + getAutocompleteOptions: () => Promise.resolve([]), getPeerInfo: () => Promise.resolve(undefined), }; @@ -97,6 +103,7 @@ type MediaCallExternalContextType = { onToggleWidget: (peerInfo?: PeerInfo) => void; onEndCall: () => void; peerInfo: PeerInfo | undefined; + setOpenRoomId: (roomId: string | undefined) => void; }; type MediaCallUnauthorizedContextType = { @@ -104,6 +111,7 @@ type MediaCallUnauthorizedContextType = { onToggleWidget: undefined; onEndCall: undefined; peerInfo: undefined; + setOpenRoomId: undefined; }; type MediaCallUnlicensedContextType = { @@ -111,6 +119,7 @@ type MediaCallUnlicensedContextType = { onToggleWidget: (peerInfo?: any) => void; onEndCall: undefined; peerInfo: undefined; + setOpenRoomId: undefined; }; const MediaCallContext = createContext( @@ -146,7 +155,13 @@ export const useMediaCallExternalContext = (): return context; } - return { state: context.state, onToggleWidget: context.onToggleWidget, onEndCall: context.onEndCall, peerInfo: context.peerInfo }; + return { + state: context.state, + onToggleWidget: context.onToggleWidget, + onEndCall: context.onEndCall, + peerInfo: context.peerInfo, + setOpenRoomId: context.setOpenRoomId, + }; }; export default MediaCallContext; diff --git a/packages/ui-voip/src/context/MediaCallProvider.tsx b/packages/ui-voip/src/context/MediaCallProvider.tsx index 2c665a9eb20ae..2ca12897d604a 100644 --- a/packages/ui-voip/src/context/MediaCallProvider.tsx +++ b/packages/ui-voip/src/context/MediaCallProvider.tsx @@ -1,4 +1,4 @@ -import { AnchorPortal } from '@rocket.chat/ui-client'; +import { AnchorPortal, useGoToDirectMessage } from '@rocket.chat/ui-client'; import type { Device } from '@rocket.chat/ui-contexts'; import { useEndpoint, @@ -12,7 +12,7 @@ import { useSetting, } from '@rocket.chat/ui-contexts'; import type { ReactNode } from 'react'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; @@ -36,6 +36,7 @@ const MediaCallProvider = ({ children }: MediaCallProviderProps) => { const user = useUser(); const { t } = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); + const [openRoomId, setOpenRoomId] = useState(undefined); const setModal = useSetModal(); @@ -57,6 +58,11 @@ const MediaCallProvider = ({ children }: MediaCallProviderProps) => { const forceSIPRouting = useSetting('VoIP_TeamCollab_SIP_Integration_For_Internal_Calls'); + const onClickDirectMessage = useGoToDirectMessage( + { username: session.peerInfo && 'username' in session.peerInfo ? session.peerInfo.username : undefined }, + openRoomId, + ); + // For some reason `exhaustive-deps` is complaining that "session" is not in the dependencies // But we're only using the changeDevice method from the session // So I'll just destructure it here @@ -262,6 +268,8 @@ const MediaCallProvider = ({ children }: MediaCallProviderProps) => { hidden: session.hidden, remoteMuted: session.remoteMuted, remoteHeld: session.remoteHeld, + onClickDirectMessage, + setOpenRoomId, onMute, onHold, onDeviceChange, diff --git a/packages/ui-voip/src/context/MockedMediaCallProvider.tsx b/packages/ui-voip/src/context/MockedMediaCallProvider.tsx index f361fdb2c5a19..ed0e429e450d1 100644 --- a/packages/ui-voip/src/context/MockedMediaCallProvider.tsx +++ b/packages/ui-voip/src/context/MockedMediaCallProvider.tsx @@ -16,11 +16,13 @@ type MockedMediaCallProviderProps = { remoteHeld?: boolean; muted?: boolean; held?: boolean; + onClickDirectMessage?: () => void; }; const MockedMediaCallProvider = ({ children, state = 'closed', + onClickDirectMessage = undefined, transferredBy = undefined, remoteMuted = false, remoteHeld = false, @@ -131,6 +133,8 @@ const MockedMediaCallProvider = ({ transferredBy, muted: mutedState, held: heldState, + setOpenRoomId: () => undefined, + onClickDirectMessage, remoteMuted, remoteHeld, onMute, diff --git a/packages/ui-voip/src/hooks/index.ts b/packages/ui-voip/src/hooks/index.ts index 3f9656838500c..7e9aeb8130db0 100644 --- a/packages/ui-voip/src/hooks/index.ts +++ b/packages/ui-voip/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useDevicePermissionPrompt'; export { useDraggable } from './VoipPopupDraggable/DraggableCore'; export { useMediaCallAction } from './useMediaCallAction'; +export { useMediaCallOpenRoomTracker } from './useMediaCallOpenRoomTracker'; diff --git a/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.ts b/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.ts new file mode 100644 index 0000000000000..0059d158fb045 --- /dev/null +++ b/packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +import { useMediaCallExternalContext } from '../context/MediaCallContext'; + +export const useMediaCallOpenRoomTracker = (openRoomId?: string) => { + const { setOpenRoomId } = useMediaCallExternalContext(); + + useEffect(() => { + if (!setOpenRoomId) { + return; + } + setOpenRoomId(openRoomId); + return () => { + setOpenRoomId(undefined); + }; + }, [setOpenRoomId, openRoomId]); +}; diff --git a/packages/ui-voip/src/index.ts b/packages/ui-voip/src/index.ts index a659830740e53..ad103e6939a2f 100644 --- a/packages/ui-voip/src/index.ts +++ b/packages/ui-voip/src/index.ts @@ -2,7 +2,7 @@ export { default as MediaCallProvider } from './context/MediaCallProvider'; export { MediaCallContext, useMediaCallExternalContext as useMediaCallContext, isCallingBlocked } from './context'; export type { PeerInfo, MediaCallState } from './context'; -export { useMediaCallAction } from './hooks'; +export { useMediaCallAction, useMediaCallOpenRoomTracker } from './hooks'; export { CallHistoryContextualBar } from './views'; export type { InternalCallHistoryContact, ExternalCallHistoryContact, CallHistoryData } from './views'; diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.stories.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.stories.tsx index d97afe302369d..291574bdb1680 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.stories.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.stories.tsx @@ -62,3 +62,11 @@ export const OngoingCallWithSlotsAndRemoteStatus: StoryFn = ); }; + +export const OngoingCallWithDmButton: StoryFn = () => { + return ( + undefined}> + + + ); +}; diff --git a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx index f1827c563e69e..b07475b82c308 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx +++ b/packages/ui-voip/src/views/MediaCallWidget/OngoingCall.tsx @@ -21,8 +21,20 @@ import { useMediaCallContext } from '../../context'; const OngoingCall = () => { const { t } = useTranslation(); - const { muted, held, remoteMuted, remoteHeld, onMute, onHold, onForward, onEndCall, onTone, peerInfo, connectionState } = - useMediaCallContext(); + const { + muted, + held, + remoteMuted, + remoteHeld, + onMute, + onHold, + onForward, + onEndCall, + onTone, + peerInfo, + connectionState, + onClickDirectMessage, + } = useMediaCallContext(); const { element: keypad, buttonProps: keypadButtonProps } = useKeypad(onTone); @@ -41,6 +53,9 @@ const OngoingCall = () => { }> + {onClickDirectMessage && ( + + )} diff --git a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap index dee814d8a2f74..d9c2c8430eb0e 100644 --- a/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap +++ b/packages/ui-voip/src/views/MediaCallWidget/__snapshots__/MediaCallWidget.spec.tsx.snap @@ -31,7 +31,7 @@ exports[`renders IncomingCall without crashing 1`] = ` Incoming Call...