Compare commits

..

2 Commits

Author SHA1 Message Date
Andy Balaam
95974b53cb Update strings for the verification dialog
This completes the Element Web part of
https://github.com/element-hq/element-meta/issues/2898
2025-09-26 10:40:17 +01:00
Andy Balaam
093a708b44 Change 'Verify Session' to 'Start Verification' 2025-09-26 10:40:17 +01:00
653 changed files with 8045 additions and 22205 deletions

View File

@@ -1,6 +1,11 @@
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
extends: [
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:storybook/recommended",
],
parserOptions: {
project: ["./tsconfig.json"],
},

View File

@@ -44,7 +44,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
# Disable cache on Windows as it is slower than not caching
# https://github.com/actions/setup-node/issues/975
@@ -66,7 +66,7 @@ jobs:
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp-${{ matrix.image }}
path: webapp

View File

@@ -62,7 +62,7 @@ jobs:
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: element-web.deb
path: element-web.deb

View File

@@ -28,7 +28,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -53,7 +53,7 @@ jobs:
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: dist/develop.tar.gz

View File

@@ -37,14 +37,14 @@ jobs:
install: true
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -132,7 +132,7 @@ jobs:
cosign sign --yes ${images}
- name: Update repo description
uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
if: github.event_name != 'pull_request'
continue-on-error: true
with:
@@ -141,7 +141,7 @@ jobs:
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@5fc4efd1a4797ddb68ffd0714a238564e4cc0e6f # v4
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro

View File

@@ -33,7 +33,7 @@ jobs:
repository: matrix-org/matrix-js-sdk
path: matrix-js-sdk
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
cache-dependency-path: element-web/yarn.lock

View File

@@ -25,7 +25,7 @@ jobs:
actions: read
steps:
- name: Download HTML report
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -54,7 +54,7 @@ jobs:
with:
repository: element-hq/element-web
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -74,7 +74,7 @@ jobs:
run: VERSION=$(scripts/get-version-from-git.sh) yarn build
- name: Upload Artifact
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: webapp
path: webapp
@@ -128,12 +128,12 @@ jobs:
repository: element-hq/element-web
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
name: webapp
path: webapp
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
cache-dependency-path: yarn.lock
@@ -147,7 +147,7 @@ jobs:
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
@@ -172,7 +172,7 @@ jobs:
- name: Upload blob report to GitHub Actions Artifacts
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
path: blob-report
@@ -200,7 +200,7 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
if: inputs.skip != true
with:
cache: "yarn"
@@ -212,7 +212,7 @@ jobs:
- name: Download blob reports from GitHub Actions Artifacts
if: inputs.skip != true
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
pattern: all-blob-reports-*
path: all-blob-reports
@@ -228,7 +228,7 @@ jobs:
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
- name: Upload HTML report
if: always() && inputs.skip != true
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: html-report
path: playwright-report

View File

@@ -28,7 +28,7 @@ jobs:
Exercise caution. Use test accounts.
- name: 📥 Download artifact
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -1,34 +0,0 @@
name: Publish shared component npm package
on:
workflow_dispatch: {}
concurrency: release
jobs:
publish:
name: "Publish"
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- name: 🧮 Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- name: 🔧 Set up node environment
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
cache: "yarn"
node-version-file: ".node-version"
registry-url: "https://registry.npmjs.org"
env:
NODE_AUTH_TOKEN: ${{ secrets.ELEMENT_NPM_TOKEN }}
- name: 🛠️ Setup
# When running `install` it also calls the `prepare` step which generates
# a build
run: yarn --cwd packages/shared-components install --pure-lockfile
- name: 🚀 Publish to npm
working-directory: packages/shared-components
run: npm publish --access public --provenance

View File

@@ -27,7 +27,7 @@ jobs:
run: "sudo apt-get install -y tree"
- name: Download Diffs
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -26,51 +26,45 @@ jobs:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
- name: Install element web dependencies
run: yarn install --frozen-lockfile
- name: Build Element Web resources
# Needed to prepare language files
run: "yarn build:res"
- name: Install dependencies
working-directory: packages/shared-components
run: yarn install --frozen-lockfile
- name: Get installed Playwright version
working-directory: packages/shared-components
id: playwright
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
- name: Cache playwright binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
- name: Install Playwright browsers
working-directory: packages/shared-components
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: "yarn playwright install --with-deps --only-shell"
- name: Build Element Web resources
# Needed to prepare language files
run: "yarn build:res"
- name: Build storybook dependencies
# When the first test is ran, it will fail because the dependencies are not yet built.
# This step is to ensure that the dependencies are built before running the tests.
run: "yarn --cwd packages/shared-components test:storybook:ci"
run: "yarn test:storybook:ci"
continue-on-error: true
- name: Run Visual tests
run: "yarn --cwd packages/shared-components test:storybook:ci"
run: "yarn test:storybook:ci"
- name: Upload received images & diffs
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: received-images
path: packages/shared-components/playwright/shared-component-received
path: playwright/shared-component-received

View File

@@ -24,7 +24,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -35,16 +35,6 @@ jobs:
- name: Typecheck
run: "yarn run lint:types"
- name: Build Element Web resources
# Needed to prepare language files for shared components
run: "yarn build:res"
- name: Install Shared Component Dependencies
run: "yarn --cwd packages/shared-components install"
- name: Typecheck Shared Components
run: "yarn --cwd packages/shared-components run lint:types"
i18n_lint:
name: "i18n Check"
uses: matrix-org/matrix-web-i18n/.github/workflows/i18n_check.yml@main
@@ -79,7 +69,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -91,23 +81,13 @@ jobs:
- name: Run Linter
run: "yarn run lint:js"
- name: Build Element Web resources
# Needed to prepare language files for shared components
run: "yarn build:res"
- name: Install Shared Component Deps
run: "yarn --cwd packages/shared-components install --frozen-lockfile"
- name: Run Linter
run: "yarn --cwd packages/shared-components run lint:js"
style_lint:
name: "Style Lint"
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -125,7 +105,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"
@@ -143,7 +123,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"

View File

@@ -44,7 +44,7 @@ jobs:
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
- name: Yarn cache
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: "lts/*"
cache: "yarn"
@@ -55,7 +55,7 @@ jobs:
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
- name: Jest Cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
with:
path: /tmp/jest_cache
key: ${{ hashFiles('**/yarn.lock') }}
@@ -84,7 +84,7 @@ jobs:
- name: Upload Artifact
if: env.ENABLE_COVERAGE == 'true'
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-${{ matrix.runner }}
path: |
@@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@5530c593759f489bba08272e96986ffc571c1ea1
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

View File

@@ -12,7 +12,7 @@ jobs:
issues: write
pull-requests: write
steps:
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10
with:
operations-per-run: 100

View File

@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
cache: "yarn"
node-version: "lts/*"

4
.gitignore vendored
View File

@@ -4,6 +4,7 @@
/key.pem
/lib
/node_modules
/packages/
/webapp
/.npmrc
/*.log
@@ -33,6 +34,3 @@ electron/pub
*storybook.log
storybook-static
/packages/shared-components/node_modules
/packages/shared-components/dist

View File

@@ -1 +1 @@
24
22

View File

@@ -12,7 +12,7 @@ import { GlobeIcon } from "@storybook/icons";
// We can't import `shared/i18n.tsx` directly here.
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
import json from "../../../webapp/i18n/languages.json";
import json from "../webapp/i18n/languages.json";
const languages = Object.keys(json).filter((lang) => lang !== "default");
/**

View File

@@ -11,8 +11,8 @@ import { nodePolyfills } from "vite-plugin-node-polyfills";
import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../../../webapp"],
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../webapp"],
addons: ["@storybook/addon-docs", "@storybook/addon-designs", "@storybook/addon-a11y"],
framework: "@storybook/react-vite",
core: {
@@ -26,7 +26,7 @@ const config: StorybookConfig = {
resolve: {
alias: {
// Alias used by i18n.tsx
$webapp: path.resolve("../../webapp"),
$webapp: path.resolve("webapp"),
},
},
// Needed for counterpart to work
@@ -36,11 +36,5 @@ const config: StorybookConfig = {
},
});
},
refs: {
"compound-web": {
title: "Compound Web",
url: "https://element-hq.github.io/compound-web/",
},
},
};
export default config;

View File

@@ -1,9 +1,9 @@
import type { ArgTypes, Preview, Decorator, ReactRenderer, StrictArgs } from "@storybook/react-vite";
import "../../../res/css/shared.pcss";
import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { setLanguage } from "../src/utils/i18n";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
import { StoryContext } from "storybook/internal/csf";

View File

@@ -8,8 +8,8 @@ Please see LICENSE files in the repository root for full details.
import { waitForPageReady } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const customSnapshotsDir = `${process.cwd()}/playwright/snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/received/`;
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
/**
* @type {import('@storybook/test-runner').TestRunnerConfig}

View File

@@ -1,53 +1,3 @@
Changes in [1.12.2](https://github.com/element-hq/element-web/releases/tag/v1.12.2) (2025-10-21)
================================================================================================
## ✨ Features
* Room List: Extend the viewport to avoid so many black spots when scrolling the room list ([#30867](https://github.com/element-hq/element-web/pull/30867)). Contributed by @langleyd.
* Hide calling buttons in room header before a room is created ([#30816](https://github.com/element-hq/element-web/pull/30816)). Contributed by @Half-Shot.
* Improve invite dialog ui - Part 2 ([#30836](https://github.com/element-hq/element-web/pull/30836)). Contributed by @florianduros.
## 🐛 Bug Fixes
* Fix platform settings race condition and make auto-launch tri-state ([#30977](https://github.com/element-hq/element-web/pull/30977)). Contributed by @t3chguy.
* Fix: member count in header and member list ([#30982](https://github.com/element-hq/element-web/pull/30982)). Contributed by @florianduros.
* Fix duration of voice message in timeline ([#30973](https://github.com/element-hq/element-web/pull/30973)). Contributed by @florianduros.
* Fix voice notes rendering at 00:00 when playback had not begun. ([#30961](https://github.com/element-hq/element-web/pull/30961)). Contributed by @Half-Shot.
* Improve handling of animated images, add support for AVIF animations ([#30932](https://github.com/element-hq/element-web/pull/30932)). Contributed by @t3chguy.
* Update key storage toggle when key storage status changes ([#30934](https://github.com/element-hq/element-web/pull/30934)). Contributed by @uhoreg.
* Fix jitsi widget popout ([#30908](https://github.com/element-hq/element-web/pull/30908)). Contributed by @dbkr.
* Improve keyboard navigation on invite dialog ([#30930](https://github.com/element-hq/element-web/pull/30930)). Contributed by @florianduros.
* Prefer UIA flows with supported UIA stages ([#30926](https://github.com/element-hq/element-web/pull/30926)). Contributed by @richvdh.
* Enhance accessibility of dropdown ([#30928](https://github.com/element-hq/element-web/pull/30928)). Contributed by @florianduros.
* Improve accessibility of the `\<AvatarSetting> component ([#30907](https://github.com/element-hq/element-web/pull/30907)). Contributed by @MidhunSureshR.
Changes in [1.12.1](https://github.com/element-hq/element-web/releases/tag/v1.12.1) (2025-10-07)
================================================================================================
## ✨ Features
* New Room List: Change the order of filters to match those on mobile ([#30905](https://github.com/element-hq/element-web/pull/30905)). Contributed by @langleyd.
* New Room List: Don't clear filters on space change ([#30903](https://github.com/element-hq/element-web/pull/30903)). Contributed by @langleyd.
* Add release announcement for the sounds ([#30900](https://github.com/element-hq/element-web/pull/30900)). Contributed by @langleyd.
* Rich Text Editor: Add emoji suggestion support ([#30873](https://github.com/element-hq/element-web/pull/30873)). Contributed by @langleyd.
* feat: Disable session lock when running in element-desktop ([#30643](https://github.com/element-hq/element-web/pull/30643)). Contributed by @kaylendog.
* Improve invite dialog ui - Part 1 ([#30764](https://github.com/element-hq/element-web/pull/30764)). Contributed by @florianduros.
* Update Message Sound for Element ([#30804](https://github.com/element-hq/element-web/pull/30804)). Contributed by @beatdemon.
* Add new and improved ringtone ([#30761](https://github.com/element-hq/element-web/pull/30761)). Contributed by @Half-Shot.
* Disable RTE formatting buttons when the content contains a slash command ([#30802](https://github.com/element-hq/element-web/pull/30802)). Contributed by @langleyd.
## 🐛 Bug Fixes
* New Room List: Improve robustness of keyboard navigation ([#30888](https://github.com/element-hq/element-web/pull/30888)). Contributed by @langleyd.
* Fix a11y issue on list in invite dialog ([#30878](https://github.com/element-hq/element-web/pull/30878)). Contributed by @florianduros.
* Switch Export and Import Icons to match intuition ([#30805](https://github.com/element-hq/element-web/pull/30805)). Contributed by @micartey.
* Hide breadcrumb option when new room list is enabled ([#30869](https://github.com/element-hq/element-web/pull/30869)). Contributed by @florianduros.
* Avoid creating multiple call objects for the same widget ([#30839](https://github.com/element-hq/element-web/pull/30839)). Contributed by @robintown.
* Add a test for #29882, which is fixed by matrix-org/matrix-js-sdk#5016 ([#30835](https://github.com/element-hq/element-web/pull/30835)). Contributed by @andybalaam.
* fix: use `help_encryption_url` of config instead of hardcoded `https://element.io/help#encryption5` ([#30746](https://github.com/element-hq/element-web/pull/30746)). Contributed by @florianduros.
* Fix html export when feature\_jump\_to\_date is enabled ([#30828](https://github.com/element-hq/element-web/pull/30828)). Contributed by @langleyd.
* Fix #30439: "Forgot recovery key" should go to "reset" ([#30771](https://github.com/element-hq/element-web/pull/30771)). Contributed by @andybalaam.
Changes in [1.12.0](https://github.com/element-hq/element-web/releases/tag/v1.12.0) (2025-09-23)
================================================================================================
## 🦖 Deprecations

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.19-labs@sha256:dce1c693ef318bca08c964ba3122ae6248e45a1b96d65c4563c8dc6fe80349a2
# syntax=docker.io/docker/dockerfile:1.18-labs@sha256:79cdc14e1c220efb546ad14a8ebc816e3277cd72d27195ced5bebdd226dd1025
# Builder
FROM --platform=$BUILDPLATFORM node:24-bullseye@sha256:c102f42d665c164b4e5e5549813b1547ac8a9f1d343c7d17ddac106905a1c30b AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f8c398a3ad2612293e8827915c056ed0f5cc708b0f676274bb6c732e3c10f93d AS builder
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
ARG USE_CUSTOM_SDKS=false
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:65a7f97c299b919190e96e38e2ff8358132732000d3bc5c00c07cc8763fca53f
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:14b127ed799301a21a1798516443c675237120c76b9a738d43c5e4747de4b1c9
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -18,7 +18,7 @@ This is anywhere your data or business logic comes from. If your view model is a
#### View
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/packages/shared-components). Develop it in storybook!
1. Located in [`shared-components`](https://github.com/element-hq/element-web/tree/develop/src/shared-components). Develop it in storybook!
2. Views are simple react components (eg: `FooView`).
3. Views use [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore) internally where the view model is the external store.
4. Views should define the interface of the view model they expect:
@@ -35,7 +35,7 @@ This is anywhere your data or business logic comes from. If your view model is a
}
// ViewModel is a type defining the methods needed for `useSyncExternalStore`
// https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/ViewModel.ts
// https://github.com/element-hq/element-web/blob/develop/src/shared-components/ViewModel.ts
type FooViewModel = ViewModel<FooViewSnapshot> & FooViewActions;
interface FooViewProps {
@@ -54,7 +54,7 @@ This is anywhere your data or business logic comes from. If your view model is a
```
5. Multiple views can share the same view model if necessary.
6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.tsx)
6. A full example is available [here](https://github.com/element-hq/element-web/blob/develop/src/shared-components/audio/AudioPlayerView/AudioPlayerView.tsx)
#### View Model

View File

@@ -11,7 +11,7 @@ There are some exceptions like when using localhost, which is considered a [secu
1. Download the latest version from <https://github.com/element-hq/element-web/releases>
1. Untar the tarball on your web server
1. Move (or symlink) the `element-x.x.x` directory to an appropriate name
1. Configure the correct caching headers in your webserver (see [README.md](../README.md#caching-requirements))
1. Configure the correct caching headers in your webserver (see below)
1. Configure the app by copying `config.sample.json` to `config.json` and
modifying it. See the [configuration docs](config.md) for details.
1. Enter the URL into your browser and log into Element!

View File

@@ -17,7 +17,7 @@ const config: Config = {
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
customExportConditions: ["browser", "node"],
},
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/packages/*/src/**/*.test.[t]s?(x)"],
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/src/shared-components/**/*.test.[t]s?(x)"],
globalSetup: "<rootDir>/test/globalSetup.ts",
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
@@ -41,12 +41,9 @@ const config: Config = {
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
},
transformIgnorePatterns: [
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
],
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error)).+$"],
collectCoverageFrom: [
"<rootDir>/src/**/*.{js,ts,tsx}",
"<rootDir>/packages/**/*.{js,ts,tsx}",
// getSessionLock is piped into a different JS context via stringification, and the coverage functionality is
// not available in that contest. So, turn off coverage instrumentation for it.
"!<rootDir>/src/utils/SessionLock.ts",

View File

@@ -19,8 +19,6 @@ export default {
"src/hooks/useTimeout.ts",
"src/components/views/elements/InfoTooltip.tsx",
"src/components/views/elements/StyledCheckbox.tsx",
"packages/**/*",
],
ignoreDependencies: [
// Required for `action-validator`

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.12.2",
"version": "1.12.0",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -29,7 +29,7 @@
"UserFriendlyError"
],
"scripts": {
"i18n": "matrix-gen-i18n src res packages/shared-components && yarn i18n:sort && yarn i18n:lint",
"i18n": "matrix-gen-i18n && yarn i18n:sort && yarn i18n:lint",
"i18n:sort": "jq --sort-keys '.' src/i18n/strings/en_EN.json > src/i18n/strings/en_EN.json.tmp && mv src/i18n/strings/en_EN.json.tmp src/i18n/strings/en_EN.json",
"i18n:lint": "matrix-i18n-lint && prettier --log-level=silent --write src/i18n/strings/ --ignore-path /dev/null",
"i18n:diff": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && yarn i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
@@ -65,23 +65,28 @@
"coverage": "yarn test --coverage",
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
"postinstall": "patch-package"
"postinstall": "patch-package",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
},
"resolutions": {
"**/pretty-format/react-is": "19.2.0",
"@playwright/test": "1.56.1",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001751",
"caniuse-lite": "1.0.30001741",
"testcontainers": "^11.0.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@element-hq/element-web-module-api": "1.5.0",
"@element-hq/element-web-module-api": "1.4.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
@@ -111,7 +116,7 @@
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "11.0.13",
"filesize": "11.0.2",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@@ -122,7 +127,6 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-html": "4.3.2",
"linkify-react": "4.3.2",
"linkify-string": "4.3.2",
"linkifyjs": "4.3.2",
@@ -138,7 +142,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.280.1",
"posthog-js": "1.265.1",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -146,7 +150,6 @@
"react-blurhash": "^0.3.0",
"react-dom": "^19.0.0",
"react-focus-lock": "^2.5.1",
"react-merge-refs": "^3.0.2",
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtuoso": "^4.14.0",
@@ -181,13 +184,19 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-call-embedded": "0.16.1",
"@element-hq/element-web-playwright-common": "^2.0.0",
"@element-hq/element-call-embedded": "0.15.0",
"@element-hq/element-web-playwright-common": "^1.4.6",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@rrweb/types": "^2.0.0-alpha.18",
"@sentry/webpack-plugin": "^4.0.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/addon-a11y": "^9.0.18",
"@storybook/addon-designs": "^10.0.1",
"@storybook/addon-docs": "^9.0.12",
"@storybook/icons": "^1.4.0",
"@storybook/react-vite": "^9.0.15",
"@storybook/test-runner": "^0.23.0",
"@stylistic/eslint-plugin": "^5.0.0",
"@svgr/webpack": "^8.0.0",
"@testing-library/dom": "^10.4.0",
@@ -214,9 +223,9 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.2.2",
"@types/react": "19.1.13",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.2.2",
"@types/react-dom": "19.1.9",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/sdp-transform": "^2.4.10",
@@ -240,14 +249,15 @@
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-deprecate": "0.8.7",
"eslint-plugin-deprecate": "0.8.5",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^29.0.0",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "^3.0.0",
"eslint-plugin-matrix-org": "^2.0.2",
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-storybook": "^9.0.12",
"eslint-plugin-unicorn": "^56.0.0",
"express": "^5.0.0",
"fake-indexeddb": "^6.0.0",
@@ -260,6 +270,7 @@
"jest": "^29.6.2",
"jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.7.0",
"jest-image-snapshot": "^6.5.1",
"jest-mock": "^29.6.2",
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
@@ -288,7 +299,7 @@
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"storybook": "^9.1.10",
"storybook": "^9.0.12",
"stylelint": "^16.23.0",
"stylelint-config-standard": "^39.0.0",
"stylelint-scss": "^6.0.0",
@@ -298,6 +309,8 @@
"ts-node": "^10.9.1",
"typescript": "5.8.3",
"util": "^0.12.5",
"vite": "^7.0.1",
"vite-plugin-node-polyfills": "^0.24.0",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.8.0",

View File

@@ -1,63 +0,0 @@
module.exports = {
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
extends: [
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
"plugin:storybook/recommended",
],
parserOptions: {
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,
},
rules: {
// Bind or arrow functions in props causes performance issues (but we
// currently use them in some places).
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
"matrix-org/require-copyright-header": "error",
"react-compiler/react-compiler": "error",
},
overrides: [
{
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
rules: {
"@typescript-eslint/explicit-function-return-type": [
"error",
{
allowExpressions: true,
},
],
// Remove Babel things manually due to override limitations
"@babel/no-invalid-this": ["off"],
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
// We're okay with assertion errors when we ask for them
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-object-type": [
"error",
{
// We do this sometimes to brand interfaces
allowInterfaces: "with-single-extends",
},
],
},
},
],
settings: {
react: {
version: "detect",
},
},
};

View File

@@ -1 +0,0 @@
dist/

View File

@@ -1,72 +0,0 @@
{
"name": "@element-hq/web-shared-components",
"version": "0.0.0-test.6",
"description": "Shared components for Element",
"author": "New Vector Ltd.",
"repository": {
"type": "git",
"url": "https://github.com/element-hq/element-web"
},
"exports": {
".": {
"require": {
"style": "./dist/element-web-shared-components.css",
"types": "./dist/element-web-shared-components.d.ts",
"default": "./dist/element-web-shared-components.umd.js"
},
"import": {
"style": "./dist/element-web-shared-components.css",
"types": "./dist/element-web-shared-components.d.ts",
"default": "./dist/element-web-shared-components.mjs"
}
}
},
"types": "dist/element-web-shared-components.d.ts",
"files": [
"dist",
"src",
"LICENSE",
"README.md",
"package.json"
],
"scripts": {
"postinstall": "patch-package",
"prepare": "vite build",
"storybook": "storybook dev -p 6007",
"build-storybook": "storybook build",
"lint": "yarn lint:types && yarn lint:js",
"lint:js": "eslint --max-warnings 0 src && prettier --check .",
"lint:types": "tsc --noEmit --jsx react",
"test:storybook": "test-storybook --url http://localhost:6007/",
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
},
"dependencies": {
"matrix-web-i18n": "^3.4.0",
"patch-package": "^8.0.1",
"counterpart": "^0.18.6"
},
"devDependencies": {
"@storybook/addon-a11y": "^9.1.10",
"@storybook/addon-designs": "^10.0.2",
"@storybook/addon-docs": "^9.1.10",
"@storybook/icons": "^1.6.0",
"@storybook/react-vite": "^9.1.10",
"@storybook/test-runner": "^0.23.0",
"concurrently": "^9.2.1",
"eslint": "8",
"eslint-plugin-storybook": "^10.0.0",
"jest-image-snapshot": "^6.5.1",
"patch-package": "^8.0.1",
"prettier": "^3.6.2",
"storybook": "^9.1.10",
"typescript": "^5.9.3",
"vite": "^7.1.9",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-node-polyfills": "^0.24.0"
},
"engines": {
"node": ">=20.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@@ -1,8 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
declare module "*.css";

View File

@@ -1,155 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { type KeyboardEvent } from "react";
import { renderHook } from "jest-matrix-react";
import { useListKeyboardNavigation } from "./useListKeyboardNavigation";
describe("useListKeyDown", () => {
let mockList: HTMLUListElement;
let mockItems: HTMLElement[];
let mockEvent: Partial<KeyboardEvent<HTMLUListElement>>;
beforeEach(() => {
// Create mock DOM elements
mockList = document.createElement("ul");
mockItems = [document.createElement("li"), document.createElement("li"), document.createElement("li")];
// Set up the DOM structure
mockItems.forEach((item, index) => {
item.setAttribute("tabindex", "0");
item.setAttribute("data-testid", `item-${index}`);
mockList.appendChild(item);
});
document.body.appendChild(mockList);
// Mock event object
mockEvent = {
preventDefault: jest.fn(),
key: "",
};
// Mock focus methods
mockItems.forEach((item) => {
item.focus = jest.fn();
item.click = jest.fn();
});
});
afterEach(() => {
document.body.removeChild(mockList);
jest.clearAllMocks();
});
function render(): {
current: {
listRef: React.RefObject<HTMLUListElement | null>;
onKeyDown: React.KeyboardEventHandler<HTMLUListElement>;
onFocus: React.FocusEventHandler<HTMLUListElement>;
};
} {
const { result } = renderHook(() => useListKeyboardNavigation());
result.current.listRef.current = mockList;
return result;
}
it.each([
["Enter", "Enter"],
["Space", " "],
])("should handle %s key to click active element", (name, key) => {
const result = render();
// Mock document.activeElement
Object.defineProperty(document, "activeElement", {
value: mockItems[1],
configurable: true,
});
// Simulate key press
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
expect(mockItems[1].click).toHaveBeenCalledTimes(1);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it.each(
// key, finalPosition, startPosition
[
["ArrowDown", 1, 0],
["ArrowUp", 1, 2],
["Home", 0, 1],
["End", 2, 1],
],
)("should handle %s to focus the %inth element", (key, finalPosition, startPosition) => {
const result = render();
mockList.contains = jest.fn().mockReturnValue(true);
Object.defineProperty(document, "activeElement", {
value: mockItems[startPosition],
configurable: true,
});
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
expect(mockItems[finalPosition].focus).toHaveBeenCalledTimes(1);
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it.each([["ArrowDown"], ["ArrowUp"]])("should not handle %s when active element is not in list", (key) => {
const result = render();
mockList.contains = jest.fn().mockReturnValue(false);
const outsideElement = document.createElement("button");
Object.defineProperty(document, "activeElement", {
value: outsideElement,
configurable: true,
});
result.current.onKeyDown({
...mockEvent,
key,
} as KeyboardEvent<HTMLUListElement>);
// No item should be focused
mockItems.forEach((item) => expect(item.focus).not.toHaveBeenCalled());
expect(mockEvent.preventDefault).toHaveBeenCalledTimes(1);
});
it("should not prevent default for unhandled keys", () => {
const result = render();
result.current.onKeyDown({
...mockEvent,
key: "Tab",
} as KeyboardEvent<HTMLUListElement>);
expect(mockEvent.preventDefault).not.toHaveBeenCalled();
});
it("should focus the first item if list itself is focused", () => {
const result = render();
result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[0].focus).toHaveBeenCalledTimes(1);
});
it("should focus the selected item if list itself is focused", () => {
mockItems[1].setAttribute("aria-selected", "true");
const result = render();
result.current.onFocus({ target: mockList } as React.FocusEvent<HTMLUListElement>);
expect(mockItems[1].focus).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,92 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import {
useCallback,
useRef,
type RefObject,
type KeyboardEvent,
type KeyboardEventHandler,
type FocusEventHandler,
type FocusEvent,
} from "react";
/**
* A hook that provides keyboard navigation for a list of options.
*/
export function useListKeyboardNavigation(): {
listRef: RefObject<HTMLUListElement | null>;
onKeyDown: KeyboardEventHandler<HTMLUListElement>;
onFocus: FocusEventHandler<HTMLUListElement>;
} {
const listRef = useRef<HTMLUListElement>(null);
const onFocus = useCallback((evt: FocusEvent<HTMLUListElement>) => {
if (!listRef.current) return;
if (evt.target === listRef.current) {
// By default, focus the selected item
let selectedChild = listRef.current?.firstElementChild;
// If there is a selected item, focus that instead
for (const child of listRef.current.children) {
if (child.getAttribute("aria-selected") === "true") {
selectedChild = child;
break;
}
}
(selectedChild as HTMLElement)?.focus();
}
}, []);
const onKeyDown = useCallback((evt: KeyboardEvent<HTMLUListElement>) => {
const { key } = evt;
let handled = false;
switch (key) {
case "Enter":
case " ": {
handled = true;
(document.activeElement as HTMLElement).click();
break;
}
case "ArrowDown": {
handled = true;
const currentFocus = document.activeElement;
if (listRef.current?.contains(currentFocus) && currentFocus) {
(currentFocus.nextElementSibling as HTMLElement)?.focus();
}
break;
}
case "ArrowUp": {
handled = true;
const currentFocus = document.activeElement;
if (listRef.current?.contains(currentFocus) && currentFocus) {
(currentFocus.previousElementSibling as HTMLElement)?.focus();
}
break;
}
case "Home": {
handled = true;
(listRef.current?.firstElementChild as HTMLElement)?.focus();
break;
}
case "End": {
handled = true;
(listRef.current?.lastElementChild as HTMLElement)?.focus();
break;
}
}
if (handled) {
evt.preventDefault();
}
}, []);
return { listRef, onKeyDown, onFocus };
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
// Components
export * from "./audio/AudioPlayerView";
export * from "./audio/Clock";
export * from "./audio/PlayPauseButton";
export * from "./audio/SeekBar";
export * from "./avatar/AvatarWithDetails";
export * from "./event-tiles/TextualEventView";
export * from "./message-body/MediaBody";
export * from "./pill-input/Pill";
export * from "./pill-input/PillInput";
export * from "./rich-list/RichItem";
export * from "./rich-list/RichList";
export * from "./utils/Box";
export * from "./utils/Flex";
// Utils
export { setLanguage } from "./utils/i18n";
export * from "./utils/humanize";
export * from "./utils/DateUtils";
export * from "./utils/numbers";
// MVVM
export * from "./viewmodel";
export * from "./useMockedViewModel";
export * from "./useViewModel";

View File

@@ -1,17 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
.pill {
background-color: var(--cpd-color-bg-action-primary-rest);
padding: var(--cpd-space-1x) var(--cpd-space-1-5x) var(--cpd-space-1x) var(--cpd-space-1x);
border-radius: 99px;
}
.label {
color: var(--cpd-color-text-on-solid-primary);
font: var(--cpd-font-body-sm-medium);
}

View File

@@ -1,33 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Pill } from "./Pill";
const meta = {
title: "PillInput/Pill",
component: Pill,
tags: ["autodocs"],
args: {
label: "Pill",
children: <div style={{ width: 20, height: 20, borderRadius: "100%", backgroundColor: "#ccc" }} />,
onClick: fn(),
},
} satisfies Meta<typeof Pill>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const WithoutCloseButton: Story = {
args: {
onClick: undefined,
},
};

View File

@@ -1,26 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { composeStories } from "@storybook/react-vite";
import { render } from "jest-matrix-react";
import React from "react";
import * as stories from "./Pill.stories";
const { Default, WithoutCloseButton } = composeStories(stories);
describe("Pill", () => {
it("renders the pill", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders the pill without close button", () => {
const { container } = render(<WithoutCloseButton />);
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,62 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, { type MouseEventHandler, type JSX, type PropsWithChildren, type HTMLAttributes, useId } from "react";
import classNames from "classnames";
import { IconButton } from "@vector-im/compound-web";
import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close";
import { Flex } from "../../utils/Flex";
import styles from "./Pill.module.css";
import { _t } from "../../utils/i18n";
export interface PillProps extends Omit<HTMLAttributes<HTMLDivElement>, "onClick"> {
/**
* The text label to display inside the pill.
*/
label: string;
/**
* Optional click handler for a close button.
* If provided, a close button will be rendered.
*/
onClick?: MouseEventHandler<HTMLButtonElement>;
}
/**
* A pill component that can display a label and an optional close button.
* The badge can also contain child elements, such as icons or avatars.
*
* @example
* ```tsx
* <Pill label="New" onClick={() => console.log("Closed")}>
* <SomeIcon />
* </Pill>
* ```
*/
export function Pill({ className, children, label, onClick, ...props }: PropsWithChildren<PillProps>): JSX.Element {
const id = useId();
return (
<Flex
display="inline-flex"
gap="var(--cpd-space-1-5x)"
align="center"
className={classNames(styles.pill, className)}
{...props}
>
{children}
<span id={id} className={styles.label}>
{label}
</span>
{onClick && (
<IconButton aria-describedby={id} size="16px" onClick={onClick} aria-label={_t("action|delete")}>
<CloseIcon color="var(--cpd-color-icon-tertiary)" />
</IconButton>
)}
</Flex>
);
}

View File

@@ -1,66 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Pill renders the pill 1`] = `
<div>
<div
class="flex pill"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1-5x); --mx-flex-wrap: nowrap;"
>
<div
style="width: 20px; height: 20px; border-radius: 100%; background-color: rgb(204, 204, 204);"
/>
<span
class="label"
id="_r_0_"
>
Pill
</span>
<button
aria-describedby="_r_0_"
aria-label="Delete"
class="_icon-button_1pz9o_8"
data-kind="primary"
role="button"
style="--cpd-icon-button-size: 16px;"
tabindex="0"
>
<div
class="_indicator-icon_zr2a0_17"
style="--cpd-icon-button-size: 100%;"
>
<svg
color="var(--cpd-color-icon-tertiary)"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
/>
</svg>
</div>
</button>
</div>
</div>
`;
exports[`Pill renders the pill without close button 1`] = `
<div>
<div
class="flex pill"
style="--mx-flex-display: inline-flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1-5x); --mx-flex-wrap: nowrap;"
>
<div
style="width: 20px; height: 20px; border-radius: 100%; background-color: rgb(204, 204, 204);"
/>
<span
class="label"
id="_r_1_"
>
Pill
</span>
</div>
</div>
`;

View File

@@ -1,8 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export { Pill } from "./Pill";

View File

@@ -1,34 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
.pillInput {
background-color: var(--cpd-color-bg-subtle-secondary);
border-radius: 20px;
padding: var(--cpd-space-2x) var(--cpd-space-3x) var(--cpd-space-2x) var(--cpd-space-3x);
/* To match pill height in order to avoid the PillInput to grow when a pill is inserted */
min-height: 28px;
}
.pillInput:has(.input:focus) {
outline: var(--cpd-border-width-1) solid var(--cpd-color-gray-1400);
}
.input {
all: unset;
width: 100%;
flex: 1;
color: var(--cpd-color-text-primary);
}
.input::placeholder {
color: var(--cpd-color-text-secondary);
font: var(--cpd-font-body-md-regular);
}
.largerInput {
padding: var(--cpd-space-2x) 0;
}

View File

@@ -1,38 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { PillInput } from "./PillInput";
const meta = {
title: "PillInput/PillInput",
component: PillInput,
tags: ["autodocs"],
args: {
children: (
<>
<div style={{ minWidth: 162, height: 28, backgroundColor: "#ccc", borderRadius: "99px" }} />
<div style={{ minWidth: 162, height: 28, backgroundColor: "#ccc", borderRadius: "99px" }} />
</>
),
onChange: fn(),
onRemoveChildren: fn(),
inputProps: {
"placeholder": "Type something...",
"aria-label": "pill input",
},
},
} satisfies Meta<typeof PillInput>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const NoChild: Story = { args: { children: undefined } };

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { render, screen } from "jest-matrix-react";
import React from "react";
import { composeStories } from "@storybook/react-vite";
import userEvent from "@testing-library/user-event";
import * as stories from "./PillInput.stories";
import { PillInput } from "./PillInput";
const { Default, NoChild } = composeStories(stories);
describe("PillInput", () => {
it("renders the pill input", () => {
const { container } = render(<Default />);
expect(container).toMatchSnapshot();
});
it("renders only the input without children", () => {
const { container } = render(<NoChild />);
expect(container).toMatchSnapshot();
});
it("calls onRemoveChildren when backspace is pressed and input is empty", async () => {
const user = userEvent.setup();
const mockOnRemoveChildren = jest.fn();
render(<PillInput onRemoveChildren={mockOnRemoveChildren} />);
const input = screen.getByRole("textbox");
// Focus the input and press backspace (input should be empty by default)
await user.click(input);
await user.keyboard("{Backspace}");
expect(mockOnRemoveChildren).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,96 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import React, {
type PropsWithChildren,
type JSX,
useRef,
type KeyboardEventHandler,
type HTMLAttributes,
type HTMLProps,
Children,
} from "react";
import classNames from "classnames";
import { omit } from "lodash";
import { useMergeRefs } from "react-merge-refs";
import styles from "./PillInput.module.css";
import { Flex } from "../../utils/Flex";
export interface PillInputProps extends HTMLAttributes<HTMLDivElement> {
/**
* Callback for when the user presses backspace on an empty input.
*/
onRemoveChildren?: KeyboardEventHandler;
/**
* Props to pass to the input element.
*/
inputProps?: HTMLProps<HTMLInputElement> & { "data-testid"?: string };
}
/**
* An input component that can contain multiple child elements and an input field.
*
* @example
* ```tsx
* <PillInput>
* <div>Child 1</div>
* <div>Child 2</div>
* </PillInput>
* ```
*/
export function PillInput({
className,
children,
onRemoveChildren,
inputProps,
...props
}: PropsWithChildren<PillInputProps>): JSX.Element {
const inputRef = useRef<HTMLInputElement>(null);
const inputAttributes = omit(inputProps, ["onKeyDown", "ref"]);
const ref = useMergeRefs([inputRef, inputProps?.ref]);
const hasChildren = Children.toArray(children).length > 0;
return (
<Flex
{...props}
gap="var(--cpd-space-1x)"
direction="column"
className={classNames(styles.pillInput, className)}
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
inputRef.current?.focus();
}}
>
{hasChildren && (
<Flex gap="var(--cpd-space-1x)" wrap="wrap" align="center">
{children}
</Flex>
)}
<input
ref={ref}
autoComplete="off"
className={classNames(styles.input, { [styles.largerInput]: hasChildren })}
onKeyDown={(evt) => {
const value = evt.currentTarget.value.trim();
// If the input is empty and the user presses backspace, we call the onRemoveChildren handler
if (evt.key === "Backspace" && !value) {
evt.preventDefault();
onRemoveChildren?.(evt);
return;
}
inputProps?.onKeyDown?.(evt);
}}
{...inputAttributes}
/>
</Flex>
);
}

View File

@@ -1,44 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PillInput renders only the input without children 1`] = `
<div>
<div
class="flex pillInput"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<input
aria-label="pill input"
autocomplete="off"
class="input"
placeholder="Type something..."
/>
</div>
</div>
`;
exports[`PillInput renders the pill input 1`] = `
<div>
<div
class="flex pillInput"
style="--mx-flex-display: flex; --mx-flex-direction: column; --mx-flex-align: start; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: nowrap;"
>
<div
class="flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-1x); --mx-flex-wrap: wrap;"
>
<div
style="min-width: 162px; height: 28px; background-color: rgb(204, 204, 204); border-radius: 99px;"
/>
<div
style="min-width: 162px; height: 28px; background-color: rgb(204, 204, 204); border-radius: 99px;"
/>
</div>
<input
aria-label="pill input"
autocomplete="off"
class="input largerInput"
placeholder="Type something..."
/>
</div>
</div>
`;

View File

@@ -1,8 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export { PillInput } from "./PillInput";

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
import { useMemo } from "react";
import { MockViewModel, type ViewModel } from "./viewmodel";
/**
* Hook helper to return a mocked view model created with the given snapshot and actions.
* This is useful for testing components in isolation with a mocked view model and allows to use primitive types in stories.
*
* @param snapshot
* @param actions
*/
export function useMockedViewModel<S, A>(snapshot: S, actions: A): ViewModel<S> & A {
return useMemo(() => {
const vm = new MockViewModel<S>(snapshot);
Object.assign(vm, actions);
return vm as unknown as ViewModel<S> & A;
}, [snapshot, actions]);
}

View File

@@ -1,13 +0,0 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
export * from "./BaseViewModel";
export * from "./Disposables";
export * from "./Snapshot";
export * from "./ViewModelSubscriptions";
export type * from "./ViewModel";
export * from "./MockViewModel";

View File

@@ -1,31 +0,0 @@
{
"compilerOptions": {
"allowImportingTsExtensions": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"useDefineForClassFields": true,
"module": "es2022",
"moduleResolution": "node",
"target": "es2022",
"noUnusedLocals": true,
"sourceMap": false,
"outDir": "./lib",
"declaration": true,
"jsx": "react",
"lib": ["es2022", "es2024.promise", "dom", "dom.iterable"],
"strict": true,
"paths": {
"jest-matrix-react": ["../../test/test-utils/jest-matrix-react"],
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
}
},
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"ts-node": {
"files": true,
"moduleTypes": {
"*": "cjs"
}
}
}

View File

@@ -1,47 +0,0 @@
/*
*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
* /
*/
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, "src/index.ts"),
name: "Element Web Shared Components",
// the proper extensions will be added
fileName: "element-web-shared-components",
},
outDir: "dist",
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ["react", "react-dom"],
output: {
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
"react": "react",
"react-dom": "ReactDom",
},
},
},
},
resolve: {
alias: {
// Alias used by i18n.tsx
$webapp: resolve(__dirname, "..", "..", "webapp"),
},
},
plugins: [dts({ rollupTypes: true, include: ["src/**/*.{ts,tsx}"], copyDtsFiles: true })],
});

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
css: `
/* The timestamp is of inconsistent width depending on the time the test runs at */
.mx_MessageTimestamp {
visibility: hidden;
display: none !important;
}
/* The MAB showing up on hover is not needed for the test */
.mx_MessageActionBar {

View File

@@ -124,12 +124,11 @@ test.describe("HTML Export", () => {
const zip = await extractZipFileToPath(zipPath, dirPath);
await page.goto(`file://${dirPath}/${Object.keys(zip.files)[0]}/messages.html`);
await expect(page).toMatchScreenshot("html-export.png", {
mask: [page.locator(".mx_TimelineSeparator")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [
// We need to mask the whole thing because the width of the time part changes
page.locator(".mx_TimelineSeparator"),
page.locator(".mx_MessageTimestamp"),
],
});
},
);

View File

@@ -25,7 +25,9 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
await page.getByRole("menuitem", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
await page.getByRole("option", { name: bob.credentials.displayName }).click();
await expect(page.getByTestId("invite-dialog-input-wrapper").getByText("Bob")).toBeVisible();
await expect(
page.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText("Bob"),
).toBeVisible();
await page.getByRole("button", { name: "Go" }).click();
};
@@ -146,29 +148,6 @@ test.describe("Cryptography", function () {
}).toPass();
});
// When the user resets their identity, key storage also gets enabled.
// Check that the toggle updates to show the correct state.
test("Key backup status updates after resetting identity", async ({ page, app, user: aliceCredentials }) => {
await app.client.bootstrapCrossSigning(aliceCredentials);
const encryptionTab = await app.settings.openUserSettings("Encryption");
const keyStorageToggle = encryptionTab.getByRole("switch", { name: "Allow key storage" });
// Check that key storage starts off as disabled
expect(await keyStorageToggle.isChecked()).toBe(false);
// Find "the Reset cryptographic identity" button
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
// Confirm
await encryptionTab.getByRole("button", { name: "Continue" }).click();
// Enter the password
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
await page.getByRole("button", { name: "Continue" }).click();
// Key storage should now be enabled
expect(await keyStorageToggle.isChecked()).toBe(true);
});
test(
"creating a DM should work, being e2e-encrypted / user verification",
{ tag: "@screenshot" },

View File

@@ -1,59 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { createNewInstance } from "@element-hq/element-web-playwright-common";
import { expect, test } from "../../element-web-test";
import { ElementAppPage } from "../../pages/ElementAppPage";
import { createRoom, sendMessageInCurrentRoom } from "./utils";
test.use({
displayName: "Alice",
labsFlags: ["feature_share_history_on_invite"],
});
/** Tests for MSC4268: encrypted history sharing */
test.describe("History sharing", function () {
test(
"We should share history when sending invites",
{ tag: "@screenshot" },
async (
{ labsFlags, browser, page: alicePage, user: aliceCredentials, app: aliceElementApp, homeserver },
testInfo,
) => {
// In this test, Alice creates an encrypted room and sends an event;
// we then invite Bob, and ensure Bob can see the content.
await aliceElementApp.client.bootstrapCrossSigning(aliceCredentials);
// Register a second user, and open it in a second instance of the app
const bobCredentials = await homeserver.registerUser(`user_${testInfo.testId}_bob`, "password", "Bob");
const bobPage = await createNewInstance(browser, bobCredentials, {}, labsFlags);
const bobElementApp = new ElementAppPage(bobPage);
await bobElementApp.client.bootstrapCrossSigning(bobCredentials);
// Create the room and send a message
await createRoom(alicePage, "TestRoom", true);
await sendMessageInCurrentRoom(alicePage, "A message from Alice");
// Send the invite to Bob
await aliceElementApp.inviteUserToCurrentRoom(bobCredentials.userId);
// Bob accepts the invite
await bobPage.getByRole("option", { name: "TestRoom" }).click();
await bobPage.getByRole("button", { name: "Accept" }).click();
// Bob should now be able to decrypt the event
await expect(bobPage.getByText("A message from Alice")).toBeVisible();
const mask = [bobPage.locator(".mx_MessageTimestamp")];
await expect(bobPage.locator(".mx_RoomView_body")).toMatchScreenshot("shared-history-invite-accepted.png", {
mask,
});
},
);
});

View File

@@ -138,11 +138,7 @@ test.describe("Editing", () => {
// Take a snapshot of the dialog
await expect(dialog).toMatchScreenshot("message-edit-history-dialog.png", {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
{

View File

@@ -54,7 +54,9 @@ test.describe("Invite dialog", function () {
await other.getByRole("option", { name: botName }).click();
await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-room-with-user-pill.png");
@@ -93,7 +95,9 @@ test.describe("Invite dialog", function () {
await expect(other.getByRole("option", { name: botName }).getByText(bot.credentials.userId)).toBeVisible();
await other.getByRole("option", { name: botName }).click();
await expect(other.getByTestId("invite-dialog-input-wrapper").getByText(botName)).toBeVisible();
await expect(
other.locator(".mx_InviteDialog_userTile_pill .mx_InviteDialog_userTile_name").getByText(botName),
).toBeVisible();
// Take a snapshot of the invite dialog with a user pill
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("invite-dialog-dm-with-user-pill.png");
@@ -113,8 +117,6 @@ test.describe("Invite dialog", function () {
"rgba(0, 0, 0, 0)",
);
await expect(page.locator(".mx_RoomView")).toMatchScreenshot("send_your_first_message_view.png");
// Send a message to invite the bots
const composer = app.getComposer().locator("[contenteditable]");
await composer.fill("Hello}");

View File

@@ -27,7 +27,8 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const roomId = await app.getCurrentRoomIdFromUrl();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
// Room should have a knock join rule
await waitForRoom(page, app.client, roomId, (room) => {
@@ -43,7 +44,8 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const roomId = await app.getCurrentRoomIdFromUrl();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
await app.settings.openRoomSettings("Security & Privacy");
@@ -68,7 +70,8 @@ test.describe("Create Knock Room", () => {
await expect(page.locator(".mx_RoomHeader").getByText("Cybersecurity")).toBeVisible();
const roomId = await app.getCurrentRoomIdFromUrl();
const urlHash = await page.evaluate(() => window.location.hash);
const roomId = urlHash.replace("#/room/", "");
// Room should have a knock join rule
await waitForRoom(page, app.client, roomId, (room) => {

View File

@@ -322,13 +322,6 @@ test.describe("Room list filters and sort", () => {
return page.getByTestId("empty-room-list");
}
test("should render the primary filters", { tag: "@screenshot" }, async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await expect(primaryFilters).toMatchScreenshot("collapsed-primary-filters.png");
await getFilterExpandButton(page).click();
await expect(primaryFilters).toMatchScreenshot("expanded-primary-filters.png");
});
test(
"should render the default placeholder when there is no filter",
{ tag: "@screenshot" },

View File

@@ -252,26 +252,6 @@ test.describe("Room list", () => {
// Focus should be back on the notification button
await expect(notificationButton).toBeFocused();
});
test("should navigate to the top and then bottom of the room list", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const topRoom = roomListView.getByRole("option", { name: "Open room room29" });
// open the room
await topRoom.click();
// put focus back on the room list item
await topRoom.click();
await expect(topRoom).toBeFocused();
await page.keyboard.press("End");
const bottomRoom = roomListView.getByRole("option", { name: "Open room room0" });
await expect(bottomRoom).toBeFocused();
await page.keyboard.press("Home");
const topRoomAgain = roomListView.getByRole("option", { name: "Open room room29" });
await expect(topRoomAgain).toBeFocused();
});
});
});

View File

@@ -51,7 +51,6 @@ export async function doTokenRegistration(
await expect(page.getByRole("heading", { name: "Welcome Alice", exact: true })).toBeVisible();
return page.evaluate(() => ({
homeserverBaseUrl: window.mxMatrixClientPeg.get().getHomeserverUrl(),
accessToken: window.mxMatrixClientPeg.get().getAccessToken(),
userId: window.mxMatrixClientPeg.get().getUserId(),
deviceId: window.mxMatrixClientPeg.get().getDeviceId(),

View File

@@ -59,11 +59,9 @@ async function editMessage(page: Page, message: Locator, newMsg: string): Promis
}
const screenshotOptions = (page?: Page) => ({
mask: page ? [page.locator(".mx_MessageTimestamp")] : undefined,
// Hide the jump to bottom button in the timeline to avoid flakiness
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_JumpToBottomButton {
display: none !important;
}

View File

@@ -11,12 +11,10 @@ import fs from "node:fs";
import { test, expect } from "../../element-web-test";
const screenshotOptions = (page: Page) => ({
mask: [page.locator(".mx_MessageTimestamp")],
// Hide the jump to bottom button in the timeline to avoid flakiness
// Exclude timestamp and read marker from snapshot
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_JumpToBottomButton {
display: none !important;
}

View File

@@ -6,10 +6,9 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Config } from "@element-hq/element-web-playwright-common";
import { type Config, CONFIG_JSON } from "@element-hq/element-web-playwright-common";
import { type Browser, type Page } from "@playwright/test";
import { type StartedHomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers/HomeserverContainer";
import { routeConfigJson } from "@element-hq/element-web-playwright-common";
import { test, expect } from "../../element-web-test.ts";
import { logInAccountMas, registerAccountMas } from ".";
@@ -243,6 +242,17 @@ async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedD
*/
async function newContext(browser: Browser, config: Partial<Partial<Config>>, homeserver: StartedHomeserverContainer) {
const otherContext = await browser.newContext();
await routeConfigJson(otherContext, homeserver.baseUrl, config);
await otherContext.route(`http://localhost:8080/config.json*`, async (route) => {
const json = {
...CONFIG_JSON,
...config,
default_server_config: {
"m.homeserver": {
base_url: homeserver.baseUrl,
},
},
};
await route.fulfill({ json });
});
return otherContext;
}

View File

@@ -93,12 +93,10 @@ test.describe("permalinks", () => {
getPill(timeline, danielleId);
await expect(timeline).toMatchScreenshot("permalink-rendering.png", {
// Exclude timestamps from the snapshot, for consistency.
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [
// Exclude timestamps from the snapshot, for consistency.
page.locator(".mx_MessageTimestamp"),
],
});
});
});

View File

@@ -121,11 +121,7 @@ test.describe("Polls", () => {
.filter({ hasText: pollParams.title })
.getAttribute("data-scroll-tokens");
await expect(getPollTile(page, pollId)).toMatchScreenshot("Polls_Timeline_tile_no_votes.png", {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
// Bot votes 'Maybe' in the poll
@@ -297,11 +293,7 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_bubble_layout.png",
{
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
},
);
@@ -311,11 +303,7 @@ test.describe("Polls", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_a_poll_on_group_layout.png",
{
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
},
);

View File

@@ -32,26 +32,20 @@ test.describe("Release announcement", () => {
// dismiss the toast so the announcement appears
await page.getByRole("button", { name: "Dismiss" }).click();
const newSoundsName = "Weve refreshed your sounds";
// The new sounds release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(newSoundsName);
// Hide the new sounds release announcement
const newSoundsDialog = util.getReleaseAnnouncement(newSoundsName);
await newSoundsDialog.getByRole("button", { name: "OK" }).click();
const name = "Chats has a new look!";
const newRoomListName = "Chats has a new look!";
// The new room list release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(newRoomListName);
// Hide the new room list release announcement
const dialog = util.getReleaseAnnouncement(newRoomListName);
// The release announcement should be displayed
await util.assertReleaseAnnouncementIsVisible(name);
// Hide the release announcement
const dialog = util.getReleaseAnnouncement(name);
await dialog.getByRole("button", { name: "Next" }).click();
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
await util.assertReleaseAnnouncementIsNotVisible(name);
await page.reload();
await expect(page.getByRole("button", { name: "Room options" })).toBeVisible();
// Check that once the release announcements has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(newRoomListName);
// Check that once the release announcement has been marked as viewed, it does not appear again
await util.assertReleaseAnnouncementIsNotVisible(name);
},
);
});

View File

@@ -126,12 +126,7 @@ test.describe("FilePanel", () => {
// Take a snapshot of file tiles list on FilePanel
await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", {
// Exclude timestamps & flaky seek bar from snapshot
mask: [page.getByTestId("audio-player-seek")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")],
});
});

View File

@@ -31,13 +31,8 @@ test.describe("Preferences user settings tab", () => {
// Assert that the top heading is rendered
await expect(tab.getByRole("heading", { name: "Preferences" })).toBeVisible();
await expect(tab).toMatchScreenshot("Preferences-user-settings-tab-should-be-rendered-properly-1.png", {
// masked with fixed-width due to daylight saving time making the text content vary
// masked due to daylight saving time
mask: [tab.locator("#mx_dropdownUserTimezone_value")],
css: `
#mx_dropdownUserTimezone_value {
width: 200px;
}
`,
});
});

View File

@@ -361,9 +361,7 @@ test.describe("Sliding Sync", () => {
await expect(page.locator(".mx_ReplyPreview")).toBeVisible();
// now click on the permalink for Permalink me
const tile = page.locator(".mx_EventTile").filter({ hasText: "Permalink me" });
await tile.hover();
await tile.locator("a").dispatchEvent("click");
await page.locator(".mx_EventTile").filter({ hasText: "Permalink me" }).locator("a").dispatchEvent("click");
// make sure it is now selected with the little green |
await expect(page.locator(".mx_EventTile_selected").filter({ hasText: "Permalink me" })).toBeVisible();

View File

@@ -39,12 +39,7 @@ test.describe("Threads", () => {
const ThreadViewGroupSpacingStart = "56px"; // --ThreadView_group_spacing-start
// Exclude timestamp and read marker from snapshots
const mask = [page.locator(".mx_MessagePanel_myReadMarker")];
const css = `
.mx_MessageTimestamp {
visibility: hidden;
}
`;
const mask = [page.locator(".mx_MessageTimestamp"), page.locator(".mx_MessagePanel_myReadMarker")];
const roomViewLocator = page.locator(".mx_RoomView_body");
// User sends message
@@ -79,15 +74,13 @@ test.describe("Threads", () => {
// Take snapshots in group layout and bubble layout (IRC layout is not available on ThreadView)
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_group_layout.png", {
mask,
css,
mask: mask,
});
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
await expect(page.locator(".mx_ThreadView .mx_EventTile[data-layout='bubble']")).toHaveCount(2);
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot("Initial_ThreadView_on_bubble_layout.png", {
mask,
css,
mask: mask,
});
// Set the group layout
@@ -161,8 +154,7 @@ test.describe("Threads", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_reaction_and_a_hidden_event_on_group_layout.png",
{
mask,
css,
mask: mask,
},
);
@@ -186,8 +178,7 @@ test.describe("Threads", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_reaction_and_a_hidden_event_on_bubble_layout.png",
{
mask,
css,
mask: mask,
},
);
@@ -223,8 +214,7 @@ test.describe("Threads", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_redacted_messages_on_group_layout.png",
{
mask,
css,
mask: mask,
},
);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Bubble);
@@ -232,8 +222,7 @@ test.describe("Threads", () => {
await expect(page.locator(".mx_ThreadView")).toMatchScreenshot(
"ThreadView_with_redacted_messages_on_bubble_layout.png",
{
mask,
css,
mask: mask,
},
);
@@ -456,7 +445,7 @@ test.describe("Threads", () => {
await expect(locator.locator(".mx_EventTile").last().getByText("Hello Mr. User")).toBeAttached();
});
test("navigate through right panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
test("navigate through right panel", async ({ page, app, user }) => {
// Create room
const roomId = await app.client.createRoom({});
await page.goto("/#/room/" + roomId);
@@ -508,9 +497,6 @@ test.describe("Threads", () => {
await expect(
threadPanel.locator(".mx_EventTile_last").getByText("Hello again Mr. User in a thread"),
).toBeVisible();
await expect(threadPanel).toMatchScreenshot("thread-panel.png", {
css: ".mx_MessageTimestamp { visibility: hidden !important; }",
});
const rightPanel = page.locator(".mx_RightPanel");
// Check that the threads are listed

View File

@@ -181,10 +181,8 @@ test.describe("Timeline", () => {
await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-irc-layout.png", {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
@@ -217,10 +215,8 @@ test.describe("Timeline", () => {
await expect(gels.getByRole("button", { name: "Collapse" })).toBeVisible();
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-modern-layout.png", {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
@@ -259,11 +255,7 @@ test.describe("Timeline", () => {
// Save snapshot of expanded generic event list summary on bubble layout
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-bubble-layout.png", {
// Exclude timestamp from snapshot
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
// Click "collapse" link button on the first hovered info event line
@@ -279,11 +271,7 @@ test.describe("Timeline", () => {
// Save snapshot of collapsed generic event list summary on bubble layout
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("collapsed-gels-bubble-layout.png", {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
},
);
@@ -324,14 +312,12 @@ test.describe("Timeline", () => {
"event-line-inline-start-margin-irc-layout.png",
{
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
},
);
await expect(axe).toHaveNoViolations();
@@ -423,11 +409,7 @@ test.describe("Timeline", () => {
"collapsed-gels-and-messages-irc-layout.png",
{
// Exclude timestamp from snapshot of mx_MainSplit
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
},
);
@@ -446,11 +428,7 @@ test.describe("Timeline", () => {
"expanded-gels-and-messages-irc-layout.png",
{
// Exclude timestamp from snapshot of mx_MainSplit
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
},
);
@@ -475,11 +453,7 @@ test.describe("Timeline", () => {
"expanded-gels-redaction-placeholder.png",
{
// Exclude timestamp from snapshot of mx_MainSplit
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
},
);
@@ -507,11 +481,7 @@ test.describe("Timeline", () => {
// Record alignment of expanded GELS, placeholder of deleted message, and emote
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot("expanded-gels-emote-irc-layout.png", {
// Exclude timestamp from snapshot of mx_MainSplit
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
},
);
@@ -522,14 +492,12 @@ test.describe("Timeline", () => {
async ({ page, app, room }) => {
const screenshotOptions = {
// Hide because flaky - See https://github.com/vector-im/element-web/issues/24957
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
};
await sendEvent(app.client, room.roomId);
@@ -637,10 +605,12 @@ test.describe("Timeline", () => {
await messageEdit(page);
// Click timestamp to highlight hidden event line
const timestamp = page.locator(".mx_RoomView_body .mx_EventTile_info a.mx_MessageTimestamp");
const timestamp = page.locator(".mx_RoomView_body .mx_EventTile_info a", {
has: page.locator(".mx_MessageTimestamp"),
});
// wait for the remote echo otherwise we get an error modal due to a 404 on the /event/ API
await expect(timestamp).not.toHaveAttribute("href", /~!/);
await timestamp.click();
await timestamp.locator(".mx_MessageTimestamp").click();
// should not add inline start padding to a hidden event line on IRC layout
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@@ -650,14 +620,12 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
};
await expect(page.locator(".mx_MainSplit")).toMatchScreenshot(
@@ -686,11 +654,7 @@ test.describe("Timeline", () => {
// Exclude timestamp from snapshot
const screenshotOptions = {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
};
await sendEvent(app.client, room.roomId);
@@ -831,10 +795,8 @@ test.describe("Timeline", () => {
await app.timeline.scrollToBottom();
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
@@ -930,13 +892,7 @@ test.describe("Timeline", () => {
const tile = page.locator(".mx_EventTile");
await expect(tile).toBeVisible();
await expect(tile).toMatchScreenshot("code-block.png", {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
});
await expect(tile).toMatchScreenshot("code-block.png", { mask: [page.locator(".mx_MessageTimestamp")] });
// Edit a code block and assert the edited code block has been correctly rendered
await tile.hover();
@@ -948,11 +904,7 @@ test.describe("Timeline", () => {
const newTile = page.locator(".mx_EventTile");
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
`,
mask: [page.locator(".mx_MessageTimestamp")],
});
});
@@ -1009,7 +961,7 @@ test.describe("Timeline", () => {
const reply = "Reply";
const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => {
// View room
await app.viewRoomById(roomId);
await page.goto(`/#/room/${roomId}`);
// Send a message
const composer = app.getComposerField();
@@ -1041,24 +993,6 @@ test.describe("Timeline", () => {
await expect(eventTileLine.getByText(reply)).toHaveCount(1);
});
test("can send a voice message", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
const composerOptions = await app.openMessageComposerOptions();
await composerOptions.getByRole("menuitem", { name: "Voice Message" }).click();
// Record an empty message
await page.waitForTimeout(3000);
const roomViewBody = page.locator(".mx_RoomView_body");
await roomViewBody
.locator(".mx_MessageComposer")
.getByRole("button", { name: "Send voice message" })
.click();
await expect(roomViewBody.locator(".mx_MVoiceMessageBody")).toMatchScreenshot("voice-message.png");
});
test("can reply with a voice message", async ({ page, app, room, context }) => {
await viewRoomSendMessageAndSetupReply(page, app, room.roomId);
@@ -1155,10 +1089,8 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
@@ -1283,14 +1215,12 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
};
// Make sure the strings do not overflow on IRC layout
@@ -1349,10 +1279,8 @@ test.describe("Timeline", () => {
// Exclude timestamp and read marker from snapshot
const screenshotOptions = {
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_MessageTimestamp {
visibility: hidden;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}

View File

@@ -1,51 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { test as base, expect } from "../../element-web-test";
import { Bot } from "../../pages/bot";
const ROOM_NAME = "Jitsi Room";
const test = base.extend<{
bot1: Bot;
bot2: Bot;
}>({
bot2: async ({ page, homeserver }, use, testInfo) => {
const bot = new Bot(page, homeserver, { displayName: `ByteBot_${testInfo.testId}` });
await bot.prepareClient(); // eagerly register the bot
await use(bot);
},
});
test.describe("Jitsi Calls", () => {
test.use({
displayName: "Jimmy",
botCreateOpts: { displayName: "Bot", autoAcceptInvites: false },
});
test("should be able to pop out a jitsi widget", async ({ page, app, bot, bot2, context }) => {
const roomId = await app.client.createRoom({
name: ROOM_NAME,
invite: [bot.credentials.userId, bot2.credentials.userId],
});
await bot.joinRoom(roomId);
await bot2.joinRoom(roomId);
// open the room
await app.viewRoomByName(ROOM_NAME);
await page.getByRole("button", { name: "Video call" }).click();
const pagePromise = context.waitForEvent("page");
await page.getByRole("button", { name: "Popout widget" }).click();
const newPage = await pagePromise;
await expect(newPage.getByRole("button", { name: "Join Conference" })).toBeVisible();
});
});

View File

@@ -69,20 +69,6 @@ export class ElementAppPage {
return await this.page.evaluate(() => navigator.clipboard.readText());
}
/**
* Get the room ID from the current URL.
*
* @returns The room ID.
* @throws if the current URL does not contain a room ID.
*/
public async getCurrentRoomIdFromUrl(): Promise<string> {
const urlHash = await this.page.evaluate(() => window.location.hash);
if (!urlHash.startsWith("#/room/")) {
throw new Error("URL hash suggests we are not in a room");
}
return urlHash.replace("#/room/", "");
}
/**
* Opens the given room by name. The room must be visible in the
* room list and the room may contain unread messages.
@@ -211,21 +197,6 @@ export class ElementAppPage {
return memberlist;
}
/**
* Open the room info panel, and use it to send an invite to the given user.
*
* @param userId - The user to invite to the room.
*/
public async inviteUserToCurrentRoom(userId: string): Promise<void> {
await this.toggleRoomInfoPanel(); // TODO skip this if the room info panel is already open
await this.page.getByTestId("right-panel").getByRole("menuitem", { name: "Invite" }).click();
const input = this.page.getByRole("dialog").getByTestId("invite-dialog-input");
await input.fill(userId);
await input.press("Enter");
await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click();
}
/**
* Get a locator for the tooltip associated with an element
* @param e The element with the tooltip

View File

@@ -6,8 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type ClientServerApi, type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
export { type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
export interface HomeserverInstance {
readonly baseUrl: string;
@@ -38,4 +37,14 @@ export interface HomeserverInstance {
setThreepid(userId: string, medium: string, address: string): Promise<void>;
}
export interface Credentials {
accessToken: string;
userId: string;
deviceId: string;
homeServer: string;
password: string | null; // null for password-less users
displayName?: string;
username: string; // the localpart of the userId
}
export type HomeserverType = "synapse" | "dendrite" | "pinecone";

Some files were not shown because too many files have changed in this diff Show More