Compare commits

...

237 Commits

Author SHA1 Message Date
Michael Telatynski
01ffd72b9d Coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-08 09:28:47 +01:00
Michael Telatynski
0eee4e3a18 Coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-08 09:10:29 +01:00
Michael Telatynski
98ed46b0f3 types
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-07 16:20:39 +01:00
Michael Telatynski
78690f032d Improve coverage
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-07 15:12:04 +01:00
Michael Telatynski
607dbb03df Merge branch 'develop' of https://github.com/vector-im/element-web into t3chguy/module-api-1.3.0
# Conflicts:
#	src/stores/RoomViewStore.tsx
2025-08-07 13:51:47 +01:00
David Langley
bdfdf5fc49 Fix issue of new room list taking up the full width (#30459)
* Better handle for resizer for new room list that doesn't support collapsing.

* Add unit test

* Test the new guards/checks on resize

* Finish cleaning up mock resets
2025-08-07 11:28:11 +00:00
ElementRobot
cc094f4b56 Synchronise internationalisations with Localazy (#30407)
* [create-pull-request] automated change

* First pass of fixing tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Second pass of fixing tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Third pass of fixing tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-07 11:25:34 +00:00
Richard van der Hoff
2d0facd47b Refactor MultiInviter (#30500)
* MultiInviter: remove cancellation support

This is unused and untested, so we can basically assume it doesn't work.

* MultiInviter: factor out `handleUnknownProfileUsers` method

* MultiInviter: remove unused `ignoreProfile` arg from `inviteMore`

* MultiInviter: simplify `deferred` usage

No point in doing `deferred.resolve(this.completionStates)` everywhere

* MultiInviter.doInvite: do not `reject` for known fatal errors

Using `reject` for known, handled, fatal errors is somewhat confusing here,
since it looks like we swallow the error. (It's actually up to the caller to
check the recoreded `errors` and report them.)

Rather than rejecting, rely on the `_fatal` flag.

* MultiInviter: move finish logic to `.invite`

... for less `deferred` complication

* MultiInviter: rewrite loop as a `for` loop

Async functions are a thing in modern javascript, and way easier to grok than a
stack of promises.
2025-08-07 10:27:27 +00:00
Robin
c53b17d291 Delegate the sending of call notifications to Element Call (#30507)
* Move Element Call event types to a more appropriate file

To remove the potential for import cycles in src/models/Call.ts, which I was accidentally creating when I tried to reference data from the RoomListStore in the ElementCall class.

* Make sure ElementCall tests clean up the call object

* Upgrade Element Call to v0.14.1

* Delegate the sending of call notifications to Element Call

As of Element Call version 0.14.0, the widget is now capable of sending call notifications itself if we just request this with the sendNotificationType URL parameter. This makes Element Web's group call code a little bit more succinct.

* Fix createRoom test
2025-08-07 09:27:53 +00:00
Florian Duros
8086262e04 Move AudioPlayer to shared components (#30386)
* feat: add `PlayPauseButton` to storybook

* feat: add generic media body

* feat: add seekbar component

* chore: add ViewWrapper to help writing stories with vm

* refactor: move `formatBytes` from `formattingUtils` into shared component

* refactor: add `className` props to `Clock`

* feat: add new audio player component

* test(e2e): add screenshots for new shared components

* feat: add AudioPlayerViewModel

* feat: use new audio player in `MAudioBody`

* refactor: remove old audio player

* test(e2e): update existing tests

* refactor: remove unused `DurationClock`

* refactor: rename `SeekBar` into `LegacySeekBar`
2025-08-07 09:02:49 +00:00
Robin
f9a0a626a6 Fix widget persistence in React development mode (#30509)
15f1291cbc was really close to making widgets just work again in React development mode following the upgrade to React 19, but I forgot to test one thing: that persistent widgets (such as Element Call) still reuse the same iframe across their entire lifecycle as expected. The solution is to not manually destroy the iframe when AppTile is being unmounted; even if it turns out that the widget isn't actually persistent, React will still destroy it automatically for us.
2025-08-07 07:44:20 +00:00
ElementRobot
d7f54355ac [create-pull-request] automated change (#30510)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-08-07 06:20:30 +00:00
renovate[bot]
a668216e20 Update dependency stylelint-config-standard to v39 (#30497)
* Update dependency stylelint-config-standard to v39

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-06 12:59:04 +00:00
renovate[bot]
1cadf1a82e Update dependency @sentry/browser to v10 (#30495)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-06 12:48:48 +00:00
R Midhun Suresh
ee37734cfc MVVM - Introduce some helpers for snapshot management (#30398)
* Introduce snapshot class to track snapshot updates

This avoids the hassle of having to manually call emit.

* Better viewmodel ergonomics

- Rename `SubscriptionViewModel` to `BaseViewModel`. I feel this is
  appropriate since that class does more than just manage subscriptions.
- `getSnapshot` is no longer an abstract method. It's simply a method
  that returns the current snapshot state. This ensures that getSnapshot
result is cached by default which is required by `useSyncExternalStore`.
- `props` are a property of the base vm class so that actual VMs don't
  have to keep creating this property.

* Update `TextualEventViewModel`

* Fix test

* Rename `TextualEvent` to `TextualEventView`

* Fix snapshot object not being merged

* Rename directory to `EventTileView`

* Fix broken snapshot

* Add test for snapshot class
2025-08-06 12:29:32 +00:00
Robin
15f1291cbc Fix widget initialization in React development mode (#30463)
Since the upgrade to React 19, widget initialization (most notably affecting group calls) has been broken in development mode. This is because React now executes all callback refs twice, and the callback ref that receives the widget's iframe was not prepared to deal with that. I've fixed this by creating and attaching the iframe to the DOM in the callback ref, which allows us to properly couple its lifetime to that of the StopGapWidget. I've also added some insurance against strict mode-style races in StopGapWidget (doesn't hurt).
2025-08-06 12:17:00 +00:00
ElementRobot
8a550cf3f6 [create-pull-request] automated change (#30503)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-08-06 08:37:31 +00:00
renovate[bot]
9c911d5c59 Update definitelyTyped (#30484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 18:07:47 +00:00
Florian Duros
6fca4d106e Move clock into shared components (#30480)
* refactor: extract `formatSeconds` from `DateUtils`

* refactor: move clock into shared-components

* refactor: update clock imports

* test(e2e): add screenshots
2025-08-05 17:04:55 +00:00
Florian Duros
24f923feac Move number.ts to utils in shared components (#30498)
* refactor: move `number.ts` in shared components

* chore: include ts test file in sonar config
2025-08-05 17:04:45 +00:00
renovate[bot]
9be2b973d0 Update react monorepo (#30486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 16:39:49 +00:00
renovate[bot]
d837d2f62d Update Node.js to 2d63e0f (#30483)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 16:04:58 +00:00
renovate[bot]
f2379878cd Update typescript-eslint monorepo to v8.39.0 (#30494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:25:54 +00:00
renovate[bot]
261d073f6d Update dependency @babel/runtime to v7.28.2 (#30489)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:25:41 +00:00
renovate[bot]
401fc63eb0 Update dependency @vector-im/compound-design-tokens to v6 (#30496)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:21:51 +00:00
renovate[bot]
51c4506431 Update dependency testcontainers to v11.5.0 (#30491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:21:25 +00:00
renovate[bot]
1de27b265b Update dependency @sentry/browser to v9.44.0 (#30490)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:21:01 +00:00
renovate[bot]
db9514760d Update testing-library monorepo (#30487)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:20:40 +00:00
renovate[bot]
4b8f404bb3 Update playwright to v1.54.2 (#30485)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:20:19 +00:00
renovate[bot]
e10b1f9222 Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to e61b77b (#30482)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:19:44 +00:00
renovate[bot]
ff87df4825 Update docker (#30481)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 15:19:36 +00:00
Andy Balaam
c1a163cbc9 Hide recovery key when prompting for verification (#30471)
* Separate security_key_title from security_key_label since they differ in designs

See https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=92-8818&t=02JILBe2n7sx7ljU-1

In parallel with this, I have updated security_key_title in localazy.

* Hide recovery key on entry screen after login
2025-08-05 14:57:40 +00:00
Michael Telatynski
1e6f9dd096 Fix race condition in flaky reply chain test (#30479)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-05 13:06:44 +00:00
David Baker
6a8493c6eb Update for compatibility with v12 rooms (#30452)
* Update for compatibility with v12 rooms

Stop using powerLevelNorm and reading PL events manually.

To support https://github.com/matrix-org/matrix-js-sdk/pull/4937

* Add test for leave space dialog

* Don't compute stuff if we don't need it

* Use room.client

* Use getSafeUserId

* Remove client arg

* Use getJoinedMembers

and add doc

* Fix tests

* Fix more tests

* Fix other test

* Clarify comment

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-05 11:10:30 +00:00
ElementRobot
12927cc4a7 [create-pull-request] automated change (#30465)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-08-05 08:28:41 +00:00
Florian Duros
814f4a85df fix: tooltip on presence (#30474) 2025-08-04 13:54:38 +00:00
Florian Duros
475504d33b New room list: change icon and label of menu item for to start a DM (#30470)
* feat: change `New message` to `Start chat` and change icon

* feat: update the room list empty states

* test: update existing tests

* test(e2e): update playwright tests
2025-08-04 12:42:05 +00:00
Florian Duros
7faee3d1b7 New room list: add tooltip for presence and room status (#30472)
* feat: add tooltip to room avatar

* test: update snapshots
2025-08-04 11:32:32 +00:00
renovate[bot]
30e7567064 Update dependency linkifyjs to v4.3.2 [SECURITY] (#30430)
* Update dependency linkifyjs to v4.3.2 [SECURITY]

* Bump the other linkify deps

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-08-03 21:20:35 +00:00
David Langley
2250f5e6a2 Fix: Clicking on an item in the member list causes it to scroll to the top rather than show the profile view (#30455)
* Fix issue and add test

* Fix MemberTileView

* Add e2e test and comment
2025-08-01 13:16:13 +00:00
Will Hunt
e43b696461 Kickoff an Element Web Pro build when a new Docker image is pushed (#30451)
* Kickoff an Element Web Pro build on successful docker push

* v3
2025-08-01 12:01:10 +00:00
ElementRobot
bf98ede4fa [create-pull-request] automated change (#30456)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-08-01 06:22:21 +00:00
David Langley
cc0ece9837 Implement the member list with virtuoso (#29869)
* implement basic scrolling and keyboard navigation

* Update focus style and improve keyboard navigation

* lint

* Use avatar tootltip for the title rather than the whole button

It's more performant and feels less glitchy than the button tooltip moving around when you scroll.

* lint

* Add tooltip for invite buttons active state

As we have for other icon based buttons in the right panel/app

* Fix location of scrollToIndex and add useCallback

* Improve voiceover experience

- As well as stylng cells, set the tabIndex(roving)
- Natively focus the div with .focus() so screen reader actually moves over the cells
- improve labels and roles

* Fix jest tests

* Add aria index/counts and remove repeating "Open" string in label

* update snapshot

* Add the rest of the keyboard navigation and handle the case when the list looses focus.

* lint and update snapshot

* lint

* Only focus first/lastFocsed cell if focus.currentTarget is the overall list.

So it isn't erroneously called during onClick of an item.

* Put back overscan and fix formatting

* Extract ListView out of MemberList

* lint and fix e2e test

* Update screenshot

It looks like it is slightly better center aligned in the new list, as if maybe it was 1 px to high with the old one.

* Fix default overscan value and add ListView tests

* Just leave the avatar as it was

* We removed the tooltip that showed power level. Removing string.

* Use key rather than index to track focus.

* Remove overscan, fix typos, fix scrollToItem logic

* Use listbox role for member list and correct position/count values to account for the separator

* Fix inadvertant scrolling of the timeline when using pageUp/pageDown

* Always set the roving tab index regardless of whether we are actually focused.

Fixes the issue of not being able to shift+t

* Add aria-hidden to items within the option to avoid the SR calling it a group.

Also

* Make sure there is a roving tab set if the last one has been removed from the list.

* Update snapshot
2025-07-31 15:49:53 +00:00
Richard van der Hoff
ab6ef2fa85 Add labs option for history sharing on invite (#30313)
* Add labs option for "share history on invite"

* Set `acceptSharedHistory` when joining a room

* set `shareEncryptedHistory` when sending an invite

* Update src/i18n/strings/en_EN.json

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-31 14:20:37 +00:00
David Baker
c79c8c836b Put the 'decrypting' tooltip back (#30446)
...when downloading encrypted attachments (regressed by https://github.com/element-hq/element-web/pull/30330).

Also adds tests for the tooltips and fix the tests so they don't pollute
mocks / dialogs.
2025-07-31 14:20:33 +00:00
ElementRobot
3f0dcaa64c Playwright Docker image updates (#30406)
* [create-pull-request] automated change

* [create-pull-request] automated change

* Bump playwright-common

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-31 10:23:44 +00:00
Robin
652e891663 Stop using deprecated Element Call URL parameters (#30422)
These deprecated parameters will be removed very soon (planned for Element Call version 0.15.0) and we no longer have to care about backward compatibility with old versions of Element Call (due to the embedding/bundling work), so now is the right time to migrate.
2025-07-30 22:41:30 +00:00
Richard van der Hoff
7eb5a29cf0 Hacky fix to the MatrixChat flakiness (#30429)
Add a sleep to let these tests clean up.
2025-07-30 20:50:19 +00:00
RiotRobot
1b38624fd8 Merge branch 'master' into develop 2025-07-30 14:26:24 +00:00
RiotRobot
d98533025a v1.11.108 2025-07-30 14:22:51 +00:00
ElementRobot
c3e5367e45 Fix downloaded attachments not being decrypted (#30433) (#30434)
* Fix downloaded attachments not being decrypted

Fixes https://github.com/element-hq/element-web/issues/30339

* Import order

(cherry picked from commit 1e15a322a5)

Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-07-30 13:57:09 +00:00
David Baker
1e15a322a5 Fix downloaded attachments not being decrypted (#30433)
* Fix downloaded attachments not being decrypted

Fixes https://github.com/element-hq/element-web/issues/30339

* Import order
2025-07-30 12:30:18 +00:00
Richard van der Hoff
452996eacf Playwright: clean up after verification test, to deflake later tests (#30425) 2025-07-30 12:11:14 +00:00
Will Hunt
ee120f2fa9 Use server name explicitly for via. (#30362)
* Use server name explicitly for via.

* lint
2025-07-29 17:40:56 +00:00
RiotRobot
94aa51dc57 Reset matrix-js-sdk back to develop branch 2025-07-29 13:08:36 +00:00
RiotRobot
e19d3dcd44 Merge branch 'master' into develop 2025-07-29 13:08:16 +00:00
RiotRobot
5a4b5418cc v1.11.107 2025-07-29 13:04:43 +00:00
RiotRobot
d1f62317ba Upgrade dependency to matrix-js-sdk@37.12.0 2025-07-29 13:01:13 +00:00
renovate[bot]
9232a220dc Update dependency filesize to v11 (#30380)
* Update dependency filesize to v11

* Update fileSize types

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-29 10:15:11 +00:00
Michael Telatynski
07e6f0af35 Fix test mocks
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-29 10:44:43 +01:00
Michael Telatynski
be318f9cb8 Update module API and remove jest stub
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-29 10:32:10 +01:00
Michael Telatynski
5ff42e8548 Merge branch 'develop' of https://github.com/vector-im/element-web into t3chguy/module-api-1.3.0
# Conflicts:
#	yarn.lock
2025-07-29 09:49:44 +01:00
Richard van der Hoff
45a2fd9d63 Re-enable matrixchat test (#30410)
Now that we have better logging for our tests
(https://github.com/element-hq/element-web/pull/30405), I'd like to re-enable
this test so we can try and understsnd what makes it fail.
2025-07-28 21:07:20 +00:00
Richard van der Hoff
7e40e3697f MatrixChat test robustness fixes (#30413)
* MatrixChat-test: clean up better in `afterEach`

Make the MatrixChat tests behave better by letting them finish their work in an
`act` in afterEach. Otherwise we can end up mounting new components during
cleanup, which run tasks in the background

* MatrixChat-test: clean up dispatcher test

This test was kicking off a dispatcher job which would then open a
UserDeviceSettings dialog once the test had finished. That would then throw
exceptions because some of the mock environment had been torn down.

We're just testing that it opens the right dialog, so better to intercept
`createDialog`.

Aso add an `act` to reduce warnings, and replace a `flushPromises` with a
`waitFor` to make the test more robust.
2025-07-28 16:32:53 +00:00
Michael Telatynski
6599601000 Bump module API
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-28 16:03:31 +01:00
David Langley
beaabd5b44 bump wysiwyg to 2.39.0 (#30421) 2025-07-28 15:21:24 +01:00
ElementRobot
db5c69e228 Fix e2e shield being invisible in white mode for encrypted room (#30408) (#30411)
Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-28 09:58:20 +01:00
Robin
a23a2c03d3 Allow Element Call to send call notifications (#30404)
* Allow Element Call to send call notifications

Currently Element Web is responsible for sending the call notification event, but this is planned to be changed soon. As of the upcoming Element Call 0.14.0 release, it will request the capability to send call notifications itself, and we should auto-approve this capability.

* Add reaction capability missing from test

Element Call does in fact request this one.
2025-07-27 13:37:10 +00:00
Richard van der Hoff
c2c040dd42 Lifecycle: add a bit more logging (#30414)
... to see what exactly it thinks is wrong with the session

This may be useful in debugging
https://github.com/element-hq/element-web/issues/30337 and
https://github.com/element-hq/element-web/issues/29708. but will likely be
useful in any case.
2025-07-25 16:17:19 +01:00
Florian Duros
c98358cb26 Fix e2e shield being invisible in white mode for encrypted room (#30408)
* fix: e2e icon for encrypted room

* test(e2e): add screenshot for grey shield in encrypted room
2025-07-25 10:56:21 +00:00
Richard van der Hoff
d384a9b71b Work around jest bug that swallows console output (#30405)
* Work around jest bug that swallows console output

Hacky workaround for https://github.com/jestjs/jest/issues/15747

* Fix unit test

* Only write logs if there are some to write

* Another test fix
2025-07-25 10:13:52 +00:00
Richard van der Hoff
fc04ad26ce Support EventShieldReason.MISMATCHED_SENDER (#30403)
The js-sdk now exposes a new event shield reason, which we should handle
correctly.
2025-07-25 09:43:40 +00:00
Michael Telatynski
6fb9098c30 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-25 09:52:27 +01:00
Michael Telatynski
9e50e59168 Fix import
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-25 09:47:19 +01:00
Florian Duros
b5160c47b3 chore: move i18n.tsx into utils folder (#30400) 2025-07-25 08:14:48 +00:00
Florian Duros
3af8273d6b fix: replace hardcoded string in poll history dialog (#30402) 2025-07-24 16:41:01 +00:00
Florian Duros
81edfece6a fix: replace hardcoded string on qr code back button (#30401) 2025-07-24 15:39:26 +00:00
Michael Telatynski
008b004976 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-24 16:25:22 +01:00
Florian Duros
ab26004c4c Change unencrypted and public pills to blue (#30399)
* feat: change unencrypted and public pill to blue

* test: update snapshots

* test(e2e): update screenshots
2025-07-24 14:52:59 +00:00
Richard van der Hoff
ffedca3954 Allow for unknown event shield reasons (#30397)
A forthcoming change to the js-sdk will add a new event shield reason. To avoid
a compile-time failure, add a `default` case to the code handling those
reasons.
2025-07-24 13:16:15 +00:00
David Baker
f7ef948cf0 Update playwright-common package (#30396)
So the shared components screenshot generator works
2025-07-24 10:45:15 +00:00
ElementRobot
ba828b2194 [create-pull-request] automated change (#30394)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-24 10:02:19 +00:00
Florian Duros
16ef503174 Storybook: add tooltip provider and sort stories (#30392)
* chore: add tooltip provider to storybook preview

* chore: order story alphabetically
2025-07-23 15:30:54 +00:00
Michael Telatynski
68a2748813 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-23 15:22:07 +01:00
Florian Duros
7bfb9818f6 Change color of public room icon (#30390)
* feat: change color of public room icon

* test: update room avatar snapshot

* test(e2E): update screenshots
2025-07-23 13:56:26 +00:00
David Baker
dcbba5ea9d Script for updating storybook screenshots (#30340)
* Script for updating storybook screenshots

Requires https://github.com/element-hq/element-modules/pull/43

* Prettier
2025-07-23 12:06:33 +00:00
ElementRobot
6b40da5779 [create-pull-request] automated change (#30384)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-23 11:24:19 +00:00
ElementRobot
941835ccf2 [create-pull-request] automated change (#30326)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-23 08:23:28 +00:00
renovate[bot]
4ec10a9b4d Update typescript-eslint monorepo to v8.37.0 (#30379)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-23 08:10:40 +00:00
renovate[bot]
6a48183a35 Update dependency @sentry/webpack-plugin to v4 (#30381)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 23:13:01 +00:00
renovate[bot]
62b080a50e Update all non-major dependencies (#30374)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:55:35 +00:00
renovate[bot]
0dc7fcc64a Update dependency @types/node to v18.19.120 (#30371)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:39:15 +00:00
renovate[bot]
354867baa7 Update playwright to v1.54.1 (#30378)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 17:01:34 +00:00
renovate[bot]
4c1e3c82e4 Update dependency testcontainers to v11.3.0 (#30377)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:33:02 +00:00
Florian Duros
1e689ac098 Move Flex & Box component into shared component folder (#30357)
* refactor: move Flex component in shared components

* refactor: update imports

* refactor: remove Flex pcss file

* fix: Flex component css override

* test: update snapshots

* fix: html export

* chore: add css module support to jest

* chore: keep old copyright

* refactor: change `mx_Flex` in `ErrorView` to `mx_ErrorView_flexContainer`

* test: update snapshots

* refactor: move Box component in shared components

* refactor: update import and css override

* test: update snapshots
2025-07-22 16:25:45 +00:00
renovate[bot]
16ab7ffbc7 Update Node.js to a803244 (#30370)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:24:07 +00:00
renovate[bot]
b35e2a8c45 Update dependency @stylistic/eslint-plugin to v5.2.0 (#30376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:13:45 +00:00
renovate[bot]
a07d5b82b3 Update dependency @sentry/browser to v9.40.0 (#30375)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:13:35 +00:00
renovate[bot]
ca1420e604 Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to 86df552 (#30369)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 16:12:35 +00:00
renovate[bot]
8e59ebb754 Update storybook monorepo to v9.0.17 (#30372)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-22 15:28:22 +00:00
Timo
cc2ee5ea78 Add toggle to hide empty state in devtools (#30352)
* Add toggle to hide empty state in devtools

* use translated string

* lint

* inverse logic(`hide`->`show`)

* move entry in i18n to correct position
2025-07-22 14:11:16 +00:00
Florian Duros
774e0e8f7b Fix color of icon button with outline (#30361)
* fix: room list header button color

* fix: room member list invite button

* test: update room list search snapshot

* test(e2e): update screenshots
2025-07-22 14:11:13 +00:00
RiotRobot
acb3d31a07 v1.11.107-rc.0 2025-07-22 13:29:36 +00:00
RiotRobot
9136332f42 Upgrade dependency to matrix-js-sdk@37.12.0-rc.0 2025-07-22 13:25:38 +00:00
dependabot[bot]
e0f5f48eef Bump form-data from 4.0.3 to 4.0.4 (#30360)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-22 12:28:53 +00:00
ElementRobot
e7a772472e [create-pull-request] automated change (#30341)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-22 08:51:21 +00:00
Marc
0a97cbaada MVVM userinfo: split header and verification components (#30214)
* feat: mvvm userinfo split header and verification

* test: add userinfoheader tests

* fix: userHeaderVerificationView verification method
2025-07-21 12:04:50 +00:00
R Midhun Suresh
8a879c7fca Message preview should show tooltip with the full message on hover (#30265)
* Add title attribute for message preview

So that the full message is shown in a tooltip on hover.

* Fix test

* Update src/components/views/rooms/RoomListPanel/RoomListItemView.tsx

Co-authored-by: Florian Duros <florianduros@element.io>

---------

Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-21 11:58:54 +00:00
Florian Duros
5b659fe2e5 fix: force ED titlebar color for new room list (#30332) 2025-07-18 13:24:19 +00:00
R Midhun Suresh
42c718666c Skip flaky test (#30338) 2025-07-18 11:35:51 +00:00
David Baker
f3a181a792 Fix shared component diff index generation
Because OF COURSE ubuntu has a version of tree from 2023 that doesn't
support the '-' to remove the first path element.
2025-07-18 10:54:07 +01:00
David Baker
148d7fc0a9 Add deployments write priv to visual test uploader 2025-07-18 09:54:46 +01:00
David Baker
e42fcb797f Add deployment env 2025-07-18 09:48:34 +01:00
ElementRobot
31fb23a170 [create-pull-request] automated change (#30335)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-18 06:19:14 +00:00
Michael Telatynski
61e266e8e5 Iterate
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-17 17:31:41 +01:00
David Baker
69c2afe8e4 Upload visual diffs from storybook tests (#30298)
* Very first pass at shared component views

Turn the trivial TextualEvent into a shared component with a separate view
model for element web. Args to view model will probably change to be more
specific and VM typer needs abstracting out into an interface, but should
give the general idea.

* Remove old TextualEvent

* Pass showHiddenEvents

Because we used it anyway, we just cheated by getting it from the context

* Factor out common view model stuff

* Move ViewModel interface into the shared components

* Add tiny wrapper hook

* Move showHiddenEvents into props fully

* Fill in stories / test

* chore: setup storybook

cherry pick edc5e87056
from florianduros/storybook

* Add TextualEvent component to storybook

* Add mock view model & snapshot

* Remove old style stories entry

* Change import

* Change import

* Prettier

* Add paxckage patch to @types/mdx

for React 19 compat

* Pass getSnapshot as getServerSnapshot too

* Maybe make sonar regognise tests as tests

* Typo

* Use storybook reacvt-vite

There's no reason to use the react-webpack plugin just because our app
is stuck on webpack, it just means we have vite as a dependency too.

* Change here too

* Workaround for incomatible types in rollup

https://github.com/rollup/rollup/issues/5199

* Remove webpack styling addon

Not necessary now we're using vite

* Hopefully do screenshot testing...

* need newer node

* quote issues

* Make it an npm script

* colons

* use right port

* Install playwright browsers

* Try without the if

* Oh right, we need the headless shell

* Pass flag to store received screenshots

and upload diffs too

* Update snapshot from received

* Include platform in snapshot / received dir

because font rendering differs between platforms

* Suffix snapshots with platform instead

like we do for playwright

* Remove unnecessary env vars

and better name

* Add some comments

* Prettier

* Fix yarn.lock

* Memoise vm creation

Co-authored-by: Florian Duros <florianduros@element.io>

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix listener interface

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix types

* Fix more types

* Revert useMemo

as this isn't a hook

* Unused import

* Add missing playwright step

* Add return type annotation

* Change to add / remove subscription callback

* Change to 'add' rather than 'subs.subscribe'

* Add cache specifier for only shell playwright browsers

* Add copyright headers

* Upload visual diffs from storybook testing

* Replace tab

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-17 16:18:08 +00:00
Michael Telatynski
a7c7c27ae8 Merge branch 'develop' of https://github.com/vector-im/element-web into t3chguy/module-api-1.3.0 2025-07-17 16:16:23 +01:00
Will Hunt
bc1effd2a2 Support rendering notification badges on platforms that do their own icon overlays (#30315)
* Support rendering a seperate overlay icon on supported platforms.

* Add required globals.

* i18n-ize

* Add tests

* lint

* lint

* lint

* update copyrights

* Fix test

* lint

* Fixup

* lint

* remove unused string

* fix test
2025-07-17 12:59:17 +00:00
David Baker
3b0c04c2e9 Add SubscriptionViewModel base class (#30297)
* Very first pass at shared component views

Turn the trivial TextualEvent into a shared component with a separate view
model for element web. Args to view model will probably change to be more
specific and VM typer needs abstracting out into an interface, but should
give the general idea.

* Remove old TextualEvent

* Pass showHiddenEvents

Because we used it anyway, we just cheated by getting it from the context

* Factor out common view model stuff

* Move ViewModel interface into the shared components

* Add tiny wrapper hook

* Move showHiddenEvents into props fully

* Fill in stories / test

* chore: setup storybook

cherry pick edc5e87056
from florianduros/storybook

* Add TextualEvent component to storybook

* Add mock view model & snapshot

* Remove old style stories entry

* Change import

* Change import

* Prettier

* Add paxckage patch to @types/mdx

for React 19 compat

* Pass getSnapshot as getServerSnapshot too

* Maybe make sonar regognise tests as tests

* Typo

* Use storybook reacvt-vite

There's no reason to use the react-webpack plugin just because our app
is stuck on webpack, it just means we have vite as a dependency too.

* Change here too

* Workaround for incomatible types in rollup

https://github.com/rollup/rollup/issues/5199

* Remove webpack styling addon

Not necessary now we're using vite

* Hopefully do screenshot testing...

* need newer node

* quote issues

* Make it an npm script

* colons

* use right port

* Install playwright browsers

* Try without the if

* Oh right, we need the headless shell

* Pass flag to store received screenshots

and upload diffs too

* Update snapshot from received

* Include platform in snapshot / received dir

because font rendering differs between platforms

* Suffix snapshots with platform instead

like we do for playwright

* Remove unnecessary env vars

and better name

* Add some comments

* Prettier

* Fix yarn.lock

* Memoise vm creation

Co-authored-by: Florian Duros <florianduros@element.io>

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix listener interface

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix types

* Fix more types

* Add a superclass that simple view models can extend

to reduce boilerplate

* Revert useMemo

as this isn't a hook

* Unused import

* Actually commit the file the branch is named after

* Add missing playwright step

* Add return type annotation

* Change to add / remove subscription callback

* Change to 'add' rather than 'subs.subscribe'

* Add cache specifier for only shell playwright browsers

* Add copyright headers

* Better comment wording

* Make amit an arrow function

so it can be passed directly as a callback

* Add a test

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-17 12:32:31 +00:00
ioalexander
77cb4b3157 Enhancement: Save image on CTRL+S (#30330)
* Save image on CTRL+S

* fixed cosmetic comments

* fixed test

* refactored out downloading functionality from buttons to useDownloadMedia hook

* ImageView CTRL+S use button component

* added CTRL+S test & lint

* removed forwardRef

* fix lint

* i18n
2025-07-17 09:53:11 +00:00
AlirezaMrtz
3e11a62a3f Add quote functionality to MessageContextMenu (#29893) (#30323)
* Add quote functionality to MessageContextMenu (#29893)

* Remove unused import of getSelectedText from strings utility in EventTile component

* Add space after quoted text in ComposerInsert action

* Add space after quoted text in MessageContextMenu test

* add new line before and after the formated text
2025-07-17 09:45:08 +00:00
ElementRobot
084f447c6e [create-pull-request] automated change (#30331)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-17 06:19:13 +00:00
Florian Duros
55c8256900 fix:put a background color to the left panel when the new room list is enabled (#30328) 2025-07-16 19:13:49 +00:00
Florian Duros
b64e9ed675 Add i18n to storybook (#30268)
* refactor: extract i18n from languageHandler to not import matrix-js-sdk, settings...

* fix: circular deps

* feat: add language selector to storybook

* fix: make visual test works in CI
2025-07-16 18:21:09 +00:00
R Midhun Suresh
dc2060fc7b Fix flaky scrolling (#30329)
There are two potential problems here:
1. mouse.scroll returns before the scroll is completed
2. visibility check does not check if the element is actually in the
   viewport.

I've added a helper function to make it easier to scroll to the end of
an infinite list.
2025-07-16 15:10:05 +00:00
Michael Telatynski
1414b8ff54 Merge branch 'develop' of https://github.com/vector-im/element-web into t3chguy/module-api-1.3.0 2025-07-16 14:56:15 +01:00
ElementRobot
0e37fea9f5 [create-pull-request] automated change (#30325)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-16 06:18:29 +00:00
RiotRobot
7bb526b83a Reset matrix-js-sdk back to develop branch 2025-07-15 15:06:00 +00:00
RiotRobot
2885fc2443 Merge branch 'master' into develop 2025-07-15 15:05:36 +00:00
RiotRobot
d05806b9e9 v1.11.106 2025-07-15 15:01:54 +00:00
RiotRobot
3f2f463bc3 Upgrade dependency to matrix-js-sdk@37.11.0 2025-07-15 14:47:04 +00:00
ElementRobot
557293af31 Fix missing image download button (#30320) (#30322)
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
Fixes https://github.com/element-hq/element-web/issues/30319
2025-07-15 15:39:46 +01:00
David Baker
114ad1df0d Fix missing image download button (#30320)
Fixes https://github.com/element-hq/element-web/issues/30319
2025-07-15 15:14:14 +01:00
ElementRobot
0fe275fbd2 [create-pull-request] automated change (#30316)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-15 06:20:27 +00:00
AlirezaMrtz
93f04f7aaa Prevent default form submission in MemberListView (#30312) 2025-07-14 13:44:03 +00:00
David Baker
4bbcb8bb5d Initial structure for shared component views (#30216)
* Very first pass at shared component views

Turn the trivial TextualEvent into a shared component with a separate view
model for element web. Args to view model will probably change to be more
specific and VM typer needs abstracting out into an interface, but should
give the general idea.

* Remove old TextualEvent

* Pass showHiddenEvents

Because we used it anyway, we just cheated by getting it from the context

* Factor out common view model stuff

* Move ViewModel interface into the shared components

* Add tiny wrapper hook

* Move showHiddenEvents into props fully

* Fill in stories / test

* chore: setup storybook

cherry pick edc5e87056
from florianduros/storybook

* Add TextualEvent component to storybook

* Add mock view model & snapshot

* Remove old style stories entry

* Change import

* Change import

* Prettier

* Add paxckage patch to @types/mdx

for React 19 compat

* Pass getSnapshot as getServerSnapshot too

* Maybe make sonar regognise tests as tests

* Typo

* Use storybook reacvt-vite

There's no reason to use the react-webpack plugin just because our app
is stuck on webpack, it just means we have vite as a dependency too.

* Change here too

* Workaround for incomatible types in rollup

https://github.com/rollup/rollup/issues/5199

* Remove webpack styling addon

Not necessary now we're using vite

* Hopefully do screenshot testing...

* need newer node

* quote issues

* Make it an npm script

* colons

* use right port

* Install playwright browsers

* Try without the if

* Oh right, we need the headless shell

* Pass flag to store received screenshots

and upload diffs too

* Update snapshot from received

* Include platform in snapshot / received dir

because font rendering differs between platforms

* Suffix snapshots with platform instead

like we do for playwright

* Remove unnecessary env vars

and better name

* Add some comments

* Prettier

* Fix yarn.lock

* Memoise vm creation

Co-authored-by: Florian Duros <florianduros@element.io>

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix listener interface

* Add implements

Co-authored-by: Florian Duros <florianduros@element.io>

* Fix types

* Fix more types

* Revert useMemo

as this isn't a hook

* Unused import

* Add missing playwright step

* Add return type annotation

* Change to add / remove subscription callback

* Change to 'add' rather than 'subs.subscribe'

* Add cache specifier for only shell playwright browsers

* Add copyright headers

---------

Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-14 13:13:02 +00:00
ElementRobot
361d36272e [create-pull-request] automated change (#30314)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-14 08:02:29 +00:00
ElementRobot
8bb1b22d46 [create-pull-request] automated change (#30311)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-12 06:18:38 +00:00
Richard van der Hoff
1090c52410 Flaky test issue auto-closer: only close playwright test issues (#30302)
Only the playwright tests are automatically updated, and are therefore safe to
auto-close.
2025-07-11 13:03:55 +00:00
ElementRobot
e528f95b2e [create-pull-request] automated change (#30307)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-11 06:19:01 +00:00
ElementRobot
f3058c9597 Fix e2e icon colour (#30299) (#30304)
* fix: remove white background on e2e verification icon and put white on the checkmark

* test(e2e): add non regression tests

* chore: remove unused CSS mask

(cherry picked from commit a05ca97409)

Co-authored-by: Florian Duros <florianduros@element.io>
2025-07-10 19:47:50 +00:00
Florian Duros
a05ca97409 Fix e2e icon colour (#30299)
* fix: remove white background on e2e verification icon and put white on the checkmark

* test(e2e): add non regression tests

* chore: remove unused CSS mask
2025-07-10 13:50:18 +00:00
Valere Fedronic
2d92b73e5f Widgets: Use the new ClientEvent.ReceivedToDeviceMessage instead of ToDeviceEvent (#30239) 2025-07-10 08:04:29 +00:00
ElementRobot
366eeb7d61 [create-pull-request] automated change (#30301)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-10 06:18:43 +00:00
Richard van der Hoff
26d71530f5 DeviceListener: add logging around key backup upload check (#30291)
* DeviceListener: add logging around key backup upload check

... in an attempt to diagnose what is going on with
https://github.com/element-hq/element-web/issues/30270

* fix typescript

* fix lint
2025-07-09 17:31:36 +00:00
renovate[bot]
7f97f46686 Update all non-major dependencies (#30281)
* Update all non-major dependencies

* Prettier

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-09 11:33:20 +00:00
ElementRobot
287a064127 [create-pull-request] automated change (#30219)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-09 08:58:44 +00:00
renovate[bot]
cfd3a968d4 Update dependency testcontainers to v11.1.0 (#30284)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 08:38:08 +00:00
renovate[bot]
6fbc2e7078 Update dependency @vector-im/compound-design-tokens to v5 (#30286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 08:28:35 +00:00
ElementRobot
31e6f15941 [create-pull-request] automated change (#30294)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-09 08:28:30 +00:00
renovate[bot]
f6e8350522 Update babel monorepo to v7.28.0 (#30282)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-09 07:36:08 +00:00
renovate[bot]
afb8e38fd7 Update typescript-eslint monorepo to v8.35.1 (#30280)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 17:45:57 +00:00
renovate[bot]
6ce5228044 Update dependency cronstrue to v3 (#30287)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 17:32:37 +00:00
renovate[bot]
a1db6f5f6e Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to ef0100e (#30273)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 16:47:10 +00:00
renovate[bot]
02f7c9b52d Update definitelyTyped (#30277)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 15:21:04 +00:00
renovate[bot]
8c7daae19f Update playwright to v1.53.2 (#30279)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 15:19:06 +00:00
renovate[bot]
c1f291347c Update dependency dotenv to v17 (#30288)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 15:14:40 +00:00
renovate[bot]
a75c5e2b2b Update dependency postcss-mixins to v12 (#30289)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:57:40 +00:00
renovate[bot]
14d1141e8d Update dependency @stylistic/eslint-plugin to v5 (#30285)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:55:43 +00:00
renovate[bot]
09cea4ad3a Update dependency @sentry/browser to v9.35.0 (#30283)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:51:24 +00:00
renovate[bot]
39dcaaaaee Update dependency @vector-im/compound-design-tokens to v4.0.5 (#30278)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:43:32 +00:00
renovate[bot]
fd45eaaa8e Update Node.js to 125996c (#30274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-08 14:43:04 +00:00
RiotRobot
3a01a00d51 v1.11.106-rc.0 2025-07-08 13:27:21 +00:00
RiotRobot
33f3ee15fe Upgrade dependency to matrix-js-sdk@37.11.0-rc.0 2025-07-08 13:23:57 +00:00
ElementRobot
df50a50741 [create-pull-request] automated change (#30269)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-08 13:23:10 +00:00
Michael Telatynski
c6a5d288ed Add missing import
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-07 12:49:35 +01:00
Michael Telatynski
8c2a035302 Add support for Module API 1.3.0
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-07 12:49:06 +01:00
Banbuii
aa2dc8e574 Fix transparent verification checkmark in dark mode (#30235)
* Fix transparent verification checkmark in dark mode

Fixes Issue https://github.com/element-hq/element-web/issues/28285

* Add white background to E2E Warning Icon

Also adapted the testcases to the new background.
2025-07-07 11:35:03 +00:00
ElementRobot
0f7e394487 [create-pull-request] automated change (#30218)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-07-07 10:39:39 +00:00
Will Hunt
9f313fcc14 Add support for module message hint allowDownloadingMedia (#30252)
* Add support for allowDownloadingMedia

* Add tests.

* Allow downloading when no event is associated.

* fix lint

* Update module API

* Update lock file too

* force CI
2025-07-07 09:03:46 +00:00
Florian Duros
1cb068a91e Fix e2e flakes in new room list (#30254)
* test: retry failing assertion in room list

* test: fix click on room not visible after scroll
2025-07-04 13:37:59 +00:00
Doug
5dd31685bb Rename the mobile_guide_app_variant config values to be clearer. (#30258)
* Fix the default mobile_guide links.

Whilst the script should update these if it fails these should link to Element X which is now the default app that we link out to from this page.

* Rename the mobile_guide_app_variant values to be clearer.

Also handle invalid config values by defaulting to Element X.

* Rename snapshots to match new app variant identifiers.
2025-07-04 12:27:30 +00:00
Hubert Chathi
9095ebdb1b Avoid using accessSecretStorage to create 4S (#30244)
* remove resetCrossSigning flag, which is no longer in use

* drop unnecessary check for cross-signing

The only place where verifyUser is called already checks that cross-signing is
set up.  (The function name is also incorrect, since it checks for the
cross-signing key, and not for 4S.)

* avoid calling accessSecretStorage to set up cross-signing or 4S

Send the user to the Encryption settings tab instead

* only create secret storage when specifically asked to

* deprecate using accessSecretStorage to create new 4S

* also remove the obsolete snapshot

* add tests

* Tweak comment

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-07-03 13:34:05 +00:00
Florian Duros
66d7c6a100 Fix release announcement test flake (#30250)
* chore: add `hideJumpToBottomButton` option to playwright screenshot

* test: hide jump to bottom button in release announcement test
2025-07-03 10:51:35 +00:00
Doug
90f4d34fbb Update the mobile_guide page to the new design and link out to Element X by default. (#30172)
* Reapply "Update the mobile_guide page to the new design. (#30006)" (#30104)

This reverts commit c51823db5e.

* Use Element X as the default mobile_guide_app_variant when omitted.

* Fix a build error on Windows.

Additionally revert "Remove unnecessary <%= require %> usages" and let webpack handle all of the assets (without a manual copy rule).

* Exclude mobile_guide from coverage gate

It has playwright tests

* Revert the re-introduction of <%= require %>

* Fix snapshot tests on mobile_guide.

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-07-03 08:28:07 +00:00
Will Hunt
e1fea71c97 Filter settings exported when rageshaking (#30236)
* Submit filtered settings to rageshakes and sentry.

* Add flag to omit some settings from being exported.

* Hide user timezone

* Hide recent searches and media event ids

* Lint

* use better wording

* lint

* Prevent language from being sent

* Add tests to check keys are prevented from being uploaded.

* don't export invite rules

* Update tests
2025-07-02 08:03:31 +00:00
RiotRobot
99f7656d09 Reset matrix-js-sdk back to develop branch 2025-07-01 15:12:56 +00:00
RiotRobot
0768534885 Merge branch 'master' into develop 2025-07-01 15:12:42 +00:00
RiotRobot
d83c619e65 v1.11.105 2025-07-01 15:09:02 +00:00
RiotRobot
fe1cddd34b Upgrade dependency to matrix-js-sdk@37.10.0 2025-07-01 14:55:45 +00:00
Simon Knott
3f931d317b Fix link to e2e docs (#30234) 2025-07-01 09:53:00 +00:00
Hubert Chathi
37df62aa4e fix typo in comment and reinstate logging of variables (#30231) 2025-06-30 21:09:50 +00:00
Hubert Chathi
3d56aa7ff6 Fix logic in DeviceListener (#30230)
* remove incorrect check for cross-signing

SETUP_ENCRYPTION tries to set up everything (4S, cross-signing and key backup),
rather than just setting up encryption, as its name would imply.
crossSigningReady == false happens when the user's device isn't verified, so it
should trigger VERIFY_THIS_SESSION rather than SETUP_ENCRYPTION

* reorder conditions in allSystemsReady to match the order in the if statements

* explicitly handle secrets missing from 4S

rather than falling back to the SETUP_ENCRYPTION catch-all.  Also, remove
SETUP_ENCRYPTION since it is no longer used.

* convert button handlers to switch statements for consistency

(almost) all the other functions that use make decisions based on Kind use
switch statements

* update i18n (remove obsolete string)
2025-06-30 14:01:06 +00:00
Marc
58875e5cf2 Mvvm split user info, create powerlevels component (#30005)
* feat: mvvm user info powerlevels

* chore: remove unecesssary comments and add new

* chore: fix lint and rebase

* fix: lint error
2025-06-30 13:26:37 +00:00
renovate[bot]
4a8b365bf8 Update playwright to v1.53.1 (#30205)
* Update playwright to v1.53.1

* Update snapshots

Presumably chrome's font rendering has changed slightly in the new major version

* Scroll until room list item is in view

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-06-27 13:46:20 +00:00
Florian Duros
18ac6b92fa test: use forceCloseAllModals instead of closeCurrentModal (#30211) 2025-06-26 08:02:42 +00:00
Robin
6ce149a7a8 Allow Element Call to learn the room name (#30213)
The latest mobile designs for Element Call have it displaying the room name in an "app bar". So the Element Call widget will soon start requesting the capability to learn the room name, and Element Web should auto-approve this capability.
2025-06-26 07:50:23 +00:00
ElementRobot
75d7a1d644 [create-pull-request] automated change (#30215)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-26 06:18:40 +00:00
Florian Duros
d0ddc92908 fix: use correct translation for content protection in settings (#30210) 2025-06-25 15:24:25 +00:00
renovate[bot]
4f13242de2 Update dependency @sentry/browser to v9.30.0 (#30204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 12:31:18 +00:00
renovate[bot]
900c4d60bc Update typescript-eslint monorepo to v8.34.1 (#30201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 12:21:28 +00:00
renovate[bot]
925f4f65c7 Update dependency @types/react to v19.1.8 (#30199)
* Update dependency @types/react to v19.1.8

* Remove patch that's no longer needed

(yay!)

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-06-25 10:59:52 +00:00
renovate[bot]
088d8121e7 Update vector-im (#30202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:57:36 +00:00
renovate[bot]
d216d68e3f Update dependency caniuse-lite to v1.0.30001724 (#30200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-25 09:37:18 +00:00
ElementRobot
f6e28cb3c7 [create-pull-request] automated change (#30208)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-25 06:23:44 +00:00
ElementRobot
434e58de52 [create-pull-request] automated change (#30207)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-25 06:17:36 +00:00
renovate[bot]
2c299fe24e Update all non-major dependencies (#30203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 17:43:32 +00:00
renovate[bot]
3965a36819 Update definitelyTyped (#30198)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 17:42:14 +00:00
renovate[bot]
d4dc89cd38 Update sigstore/cosign-installer digest to 398d4b0 (#30197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 16:47:00 +00:00
renovate[bot]
fd199b94af Update Node.js to 9ba013a (#30196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 16:46:37 +00:00
renovate[bot]
7eefb30750 Update nginxinc/nginx-unprivileged:alpine-slim Docker digest to ec6b8b1 (#30195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 16:45:36 +00:00
renovate[bot]
5486a1f235 Update guibranco/github-status-action-v2 digest to 741ea90 (#30194)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 15:55:36 +00:00
renovate[bot]
73fd91dabd Update docker (#30193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-24 15:55:15 +00:00
David Baker
e9922ee84f Support m.topic in topic update script (#30192) 2025-06-24 14:38:06 +00:00
David Baker
53eff065e4 Update the public room ID (#30191)
It got upgraded at some point but not changed here
2025-06-24 14:53:11 +01:00
Michael Telatynski
2b8f95a25b Disable file drag-and-drop if insufficient permissions (#30186)
* Disable file drag-and-drop if insufficient permissions

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-24 13:26:03 +00:00
RiotRobot
e956bb5b6d v1.11.105-rc.0 2025-06-24 12:47:31 +00:00
RiotRobot
1349726d52 Upgrade dependency to matrix-js-sdk@37.10.0-rc.0 2025-06-24 12:42:23 +00:00
Florian Duros
f707bb410e New room list: add context menu to room list item (#29952)
* chore: update compound-web

* chore: remove unused export

* feat: export content of more option menu

* feat: add context menu

* feat: add `showContextMenu` to vm

* feat: use context menu in new room list

* test: add tests for room list item

* test: fix room list test

* test: add `showContextMenu` test for `useRoomListItemViewModel`

* test: add e2e test for context menu

* chore: update compound

* test: update snapshots and e2e test

* fix: avoid icon blinking when we reopen the context menu

* test: add test for menu closing

* doc: remove useless tsdoc param

* chore: update `@vector-im/compound-web`

* refactor: remove manual focus

* test(e2e): fix focus after closing notification menu

* doc: remove useless jobs
2025-06-24 09:50:27 +00:00
ElementRobot
52f836a0dd [create-pull-request] automated change (#30190)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-24 06:18:05 +00:00
ElementRobot
c50000d124 Playwright Docker image updates (#29653)
* [create-pull-request] automated change

* Restart homeserver to clear MAS token cache

as commented

---------

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: David Baker <dbkr@users.noreply.github.com>
2025-06-23 15:57:45 +00:00
Will Hunt
0edaef3f7c Support for custom message components via Module API (#30074)
* Add new custom component api.

* Remove context menu, refactor

* fix types

* Add a test for custom modules.

* tidy

* Rewrite for new API

* Update tests

* lint

* Allow passing in props to original component

* Add hinting

* Update tests to be complete

* lint a bit more

* update docstring

* update @element-hq/element-web-module-api to 1.1.0

* fix types

* updates

* hide jump to bottom button that was causing flakes

* lint

* lint

* Use module matrix event interface instead.

* update to new module sdk

* adapt custom module sample

* Issues caught by Sonar

* lint

* fix issues

* make the comment make sense

* fix import
2025-06-23 11:55:22 +00:00
ElementRobot
ac9c6f11fb [create-pull-request] automated change (#30182)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-23 06:25:15 +00:00
Michael Telatynski
8705efec40 Use stale-screenshot-reporter from playwright-common (#30175)
* Use stale-screenshot-reporter from playwright-common

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-20 12:27:40 +00:00
ElementRobot
f5f9d68f3c [create-pull-request] automated change (#30173)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-20 06:25:36 +00:00
Michael Telatynski
a3b51edc51 Fix untranslatable string "People" in notifications beta (#30165)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-19 12:38:36 +00:00
Philipp Fruck
5ad0dceae0 docs: Remove scalar for riot.im (#30158) 2025-06-19 09:58:18 +00:00
Hubert Chathi
af984c0e80 Prompt users to set up recovery (#30075)
* Show indicator in settings dialog when user doesn't have recovery set up

* Update settings headers to use red dot for recommended settings

* update recovery setup toast and remember if the user dismisses it

* update playwright snapshots

* use typed event emitters

* reverse logic for the account data flag

* fix comment and type
2025-06-18 16:20:17 +00:00
ElementRobot
2034f8b6bb [create-pull-request] automated change (#30159)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-18 06:23:38 +00:00
RiotRobot
a7a8428d1c Reset matrix-js-sdk back to develop branch 2025-06-17 13:19:00 +00:00
RiotRobot
96797c3524 Merge branch 'master' into develop 2025-06-17 13:18:47 +00:00
RiotRobot
01519f7fd5 v1.11.104 2025-06-17 13:15:36 +00:00
RiotRobot
ba3b9840ca Upgrade dependency to matrix-js-sdk@37.9.0 2025-06-17 13:11:17 +00:00
Andy Balaam
9d1455e4dd Prevent skipping forced verification after logging in with OIDC (#30141)
Pass the freshLogin parameter along to doSetLoggedIn when restoring a session,
instead of hard-coding it to always be false.
2025-06-17 10:31:08 +00:00
ElementRobot
28a232eea8 [create-pull-request] automated change (#30144)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-16 06:23:43 +00:00
Michael Telatynski
a2bea649f6 Hide add integrations button based on UIComponent.AddIntegrations (#30140)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-06-13 09:29:13 +00:00
Florian Duros
1e3fd9d3aa Update IconButton colors (#30124)
* chore: update `@vector-im/compound-web` to 8.0.0

* refactor(IconButton): use `kind="secondary"` instead of `subtleBackground` props

* test: update snapshots

* fix: force color on room header toggle

* fix: TAC button color

* test(e2e): update release announcement screenshot
2025-06-13 08:28:43 +00:00
Marc
0f0f904cb0 Mvvm split user info, create userinfoadmintools container component (#29808)
* feat: mvvm split user info, create userinfoadmintools container component

* test: mvvm userinfoadmintools and view

* feat: user info admin components more split and comments

* test: mvvm user admin info mute view models more coverage

* chore: rename user-info folder to user_info
2025-06-13 07:08:29 +00:00
ElementRobot
d89afe83a8 [create-pull-request] automated change (#30139)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-06-13 06:22:53 +00:00
David Baker
6f0d288c1d Use nav for new room list and label sections (#30134)
* Use nav for new room list and label sections

The old room list had a nav element but it was missed in the new one,
so add it and albel the sections. Also remove the test ID and use
this instead.

* Update snapshots

* Use the function we define above
2025-06-12 15:42:30 +00:00
R Midhun Suresh
c0d91a46c7 Emit event after rebuilding home space (#30132) 2025-06-12 13:30:50 +00:00
Andy Balaam
55e874fb50 Fix #30118 - TypeError in manual verify - by using MatrixClientPeg (#30131)
We can't use MatrixClientContext inside a dialog at the moment.
2025-06-12 10:53:52 +00:00
Michael Telatynski
a622772a08 Handle m.room.pinned_events being invalid (#30129)
* Handle m.room.pinned_events being invalid

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update src/hooks/usePinnedEvents.ts

Co-authored-by: Florian Duros <florianduros@element.io>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Florian Duros <florianduros@element.io>
2025-06-12 08:12:45 +00:00
Florian Duros
389a0e689e New room list: filter list can be collapsed (#29992)
* feat: add new hook to check if a node is visible

* feat: filters in new room list can be collapsed

* feat: add animation to filter list

* feat: hide chevron when list fit on one line

* fix: use correct label for expand button

* test: update room list panel snapshots

* test: add tests for useIsNodeVisible

* chore: update i18n

* test: add tests for primary filters

* test(e2e): update existing screenshots

* test(e2e): update primary filter tests

* chore: typo in css file

* refactor: replace ternary by if in filter condition

* feat: compute filter height instead of hardcoded value

* fix: floor floating computation on filter

* refactor: move hooks to dedicated files

* test: update tests

* feat: rework collapse feature

* test: remove room list panel snapshot

* test: update room list primary filter tests

* test(e2e): update screenshots

* test(e2e): update screenshots

* test(e2e): fix favourite filter in scroll behaviour test

* fix: accessibility order when tabbing
2025-06-11 13:49:20 +00:00
R Midhun Suresh
451a99d49e Show EmptyRoomListView when low priority filter matches zero rooms (#30122)
* Implement empty section for low prio filter

* Write test
2025-06-11 11:36:29 +00:00
RiotRobot
67bd11c904 v1.11.104-rc.0 2025-06-10 13:23:03 +00:00
RiotRobot
05ffa2e5ba Upgrade dependency to matrix-js-sdk@37.9.0-rc.0 2025-06-10 13:17:49 +00:00
583 changed files with 17708 additions and 7726 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

@@ -25,26 +25,26 @@ jobs:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3
uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # 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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
@@ -53,7 +53,7 @@ jobs:
- name: Build and load
id: test-build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with:
context: .
load: true
@@ -96,7 +96,7 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
if: github.event_name != 'pull_request'
with:
images: |
@@ -110,7 +110,7 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
if: github.event_name != 'pull_request'
with:
context: .
@@ -139,3 +139,16 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: vectorim/element-web
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
if: github.event_name != 'pull_request'
with:
repository: element-hq/element-web-pro
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
event-type: image-built
# Stable way to determine the :version
client-payload: |-
{
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
}

View File

@@ -227,7 +227,7 @@ jobs:
- name: Merge into HTML Report
if: inputs.skip != true
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,./playwright/stale-screenshot-reporter.ts ./all-blob-reports
run: yarn playwright merge-reports --reporter=html,./playwright/flaky-reporter.ts,@element-hq/element-web-playwright-common/lib/stale-screenshot-reporter.js ./all-blob-reports
env:
# Only pass creds to the flaky-reporter on main branch runs
GITHUB_TOKEN: ${{ github.ref_name == 'develop' && secrets.ELEMENT_BOT_TOKEN || '' }}

View File

@@ -0,0 +1,51 @@
# Triggers after the shared component tests have finished,
# It uploads the received images and diffs to netlify, printing the URLs to the console
name: Upload Shared Component Visual Test Diffs
on:
workflow_run:
workflows: ["Shared Component Visual Tests"]
types:
- completed
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
permissions: {}
jobs:
report:
if: github.event.workflow_run.conclusion == 'failure'
name: Upload Diffs
runs-on: ubuntu-24.04
environment: Netlify
permissions:
actions: read
deployments: write
steps:
- name: Install tree
run: "sudo apt-get install -y tree"
- name: Download Diffs
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
name: received-images
path: received-images
- name: Generate Index
run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
- name: 📤 Deploy to Netlify
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
with:
path: received-images
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
revision: ${{ github.event.workflow_run.head_sha }}
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
site_id: ${{ vars.NETLIFY_SITE_ID }}
desc: Shared Component Visual Diffs
deployment_env: SharedComponentDiffs
prefix: "diffs-"

View File

@@ -0,0 +1,70 @@
name: Shared Component Visual Tests
on:
pull_request: {}
merge_group:
types: [checks_requested]
push:
branches: [develop, master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
permissions: {} # No permissions required
jobs:
testStorybook:
name: "Run Visual Tests"
runs-on: ubuntu-24.04
permissions:
actions: read
issues: read
pull-requests: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with:
persist-credentials: false
repository: element-hq/element-web
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
cache: "yarn"
node-version: "lts/*"
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Get installed Playwright version
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@5a3ec84eff668545956fd18022155c47e93e2684 # v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
- name: Install Playwright browsers
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 test:storybook:ci"
continue-on-error: true
- name: Run Visual tests
run: "yarn test:storybook:ci"
- name: Upload received images & diffs
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: received-images
path: playwright/shared-component-received

View File

@@ -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@5f2b01ce1394109f70954ae6b69ef41cf7928e63
uses: guibranco/github-status-action-v2@741ea90ba6c3ca76fe0d43ba11a90cda97d5e685
with:
authToken: ${{ secrets.GITHUB_TOKEN }}
state: success

View File

@@ -15,12 +15,14 @@ jobs:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
with:
operations-per-run: 100
# Flaky test issue closing
only-issue-labels: "Z-Flaky-Test"
any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
days-before-issue-stale: 14
days-before-issue-close: 0
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
exempt-issue-labels: "Z-Flaky-Test-Disabled"
# Stale PR closing
days-before-pr-stale: 180
days-before-pr-close: 0

View File

@@ -26,7 +26,7 @@ jobs:
env:
HS_URL: ${{ secrets.BETABOT_HS_URL }}
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
PUBLIC_ROOM_ID: "!YTvKGNlinIzlkMTVRl:matrix.org"
PUBLIC_ROOM_ID: "!IemiTbwVankHTFiEoh:matrix.org"
ANNOUNCEMENT_ROOM_ID: "!bijaLdadorKgNGtHdA:matrix.org"
TOKEN: ${{ secrets.BETABOT_ACCESS_TOKEN }}
RELEASE_STATUS: "Release status: ${{ inputs.expected_status }} expected ${{ inputs.expected_date }}"
@@ -81,6 +81,11 @@ jobs:
d.body = d.body.replace(regex, releaseTopic);
});
}
if (data["m.topic"]) {
data["m.topic"].forEach(d => {
d.body = d.body.replace(regex, releaseTopic);
});
}
res = await fetch(apiUrl, {
method: "PUT",

3
.gitignore vendored
View File

@@ -31,3 +31,6 @@ electron/pub
/index.html
# version file and tarball created by `npm pack` / `yarn pack`
/git-revision.txt
*storybook.log
storybook-static

View File

@@ -0,0 +1,28 @@
/*
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 { create } from "storybook/theming";
export default create({
base: "light",
// Colors
textColor: "#1b1d22",
colorSecondary: "#111111",
// UI
appBg: "#ffffff",
appContentBg: "#ffffff",
// Toolbar
barBg: "#ffffff",
brandTitle: "Element Web",
brandUrl: "https://github.com/element-hq/element-web",
brandImage: "https://element.io/images/logo-ele-secondary.svg",
brandTarget: "_self",
});

View File

@@ -0,0 +1,61 @@
/*
* 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 { Addon, types, useGlobals } from "storybook/manager-api";
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
import React from "react";
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";
const languages = Object.keys(json).filter((lang) => lang !== "default");
/**
* Returns the title of a language in the user's locale.
*/
function languageTitle(language: string): string {
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
}
export const languageAddon: Addon = {
title: "Language Selector",
type: types.TOOL,
render: ({ active }) => {
const [globals, updateGlobals] = useGlobals();
const selectedLanguage = globals.language || "en";
return (
<WithTooltip
placement="top"
trigger="click"
closeOnOutsideClick
tooltip={({ onHide }) => {
return (
<TooltipLinkList
links={languages.map((language) => ({
id: language,
title: languageTitle(language),
active: selectedLanguage === language,
onClick: async () => {
// Update the global state with the selected language
updateGlobals({ language });
onHide();
},
}))}
/>
);
}}
>
<IconButton title="Language">
<GlobeIcon />
{languageTitle(selectedLanguage)}
</IconButton>
</WithTooltip>
);
},
};

40
.storybook/main.ts Normal file
View File

@@ -0,0 +1,40 @@
/*
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 { StorybookConfig } from "@storybook/react-vite";
import path from "node:path";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import { mergeConfig } from "vite";
const config: StorybookConfig = {
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
staticDirs: ["../webapp"],
addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
framework: "@storybook/react-vite",
core: {
disableTelemetry: true,
},
typescript: {
reactDocgen: "react-docgen-typescript",
},
async viteFinal(config) {
return mergeConfig(config, {
resolve: {
alias: {
// Alias used by i18n.tsx
$webapp: path.resolve("webapp"),
},
},
// Needed for counterpart to work
plugins: [nodePolyfills({ include: ["process", "util"] })],
server: {
allowedHosts: ["localhost", ".docker.internal"],
},
});
},
};
export default config;

18
.storybook/manager.js Normal file
View File

@@ -0,0 +1,18 @@
/*
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 { addons } from "storybook/manager-api";
import ElementTheme from "./ElementTheme";
import { languageAddon } from "./languageAddon";
addons.setConfig({
theme: ElementTheme,
});
addons.register("elementhq/language", () => addons.add("language", languageAddon));

10
.storybook/preview.css Normal file
View File

@@ -0,0 +1,10 @@
/*
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.
*/
.docs-story {
background: var(--cpd-color-bg-canvas-default);
}

106
.storybook/preview.tsx Normal file
View File

@@ -0,0 +1,106 @@
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
import { addons } from "storybook/preview-api";
import "../res/css/shared.pcss";
import "./preview.css";
import React, { useLayoutEffect } from "react";
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
import { setLanguage } from "../src/shared-components/utils/i18n";
import { TooltipProvider } from "@vector-im/compound-web";
export const globalTypes = {
theme: {
name: "Theme",
description: "Global theme for components",
toolbar: {
icon: "circlehollow",
title: "Theme",
items: [
{ title: "System", value: "system", icon: "browser" },
{ title: "Light", value: "light", icon: "sun" },
{ title: "Light (high contrast)", value: "light-hc", icon: "sun" },
{ title: "Dark", value: "dark", icon: "moon" },
{ title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
],
},
},
language: {
name: "Language",
description: "Global language for components",
},
initialGlobals: {
theme: "system",
language: "en",
},
} satisfies ArgTypes;
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
const ThemeSwitcher: React.FC<{
theme: string;
}> = ({ theme }) => {
useLayoutEffect(() => {
document.documentElement.classList.remove(...allThemesClasses);
if (theme !== "system") {
document.documentElement.classList.add(`cpd-theme-${theme}`);
}
return () => document.documentElement.classList.remove(...allThemesClasses);
}, [theme]);
return null;
};
const withThemeProvider: Decorator = (Story, context) => {
return (
<>
<ThemeSwitcher theme={context.globals.theme} />
<Story />
</>
);
};
const LanguageSwitcher: React.FC<{
language: string;
}> = ({ language }) => {
useLayoutEffect(() => {
const changeLanguage = async (language: string) => {
await setLanguage(language);
// Force the component to re-render to apply the new language
addons.getChannel().emit(FORCE_RE_RENDER);
};
changeLanguage(language);
}, [language]);
return null;
};
export const withLanguageProvider: Decorator = (Story, context) => {
return (
<>
<LanguageSwitcher language={context.globals.language} />
<Story />
</>
);
};
const withTooltipProvider: Decorator = (Story) => {
return (
<TooltipProvider>
<Story />
</TooltipProvider>
);
};
const preview: Preview = {
tags: ["autodocs"],
decorators: [withThemeProvider, withLanguageProvider, withTooltipProvider],
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
},
};
export default preview;

37
.storybook/test-runner.js Normal file
View File

@@ -0,0 +1,37 @@
/*
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 { waitForPageReady } from "@storybook/test-runner";
import { toMatchImageSnapshot } from "jest-image-snapshot";
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
/**
* @type {import('@storybook/test-runner').TestRunnerConfig}
*/
const config = {
setup(page) {
expect.extend({ toMatchImageSnapshot });
},
async postVisit(page, context) {
await waitForPageReady(page);
// If you want to take screenshot of multiple browsers, use
// page.context().browser().browserType().name() to get the browser name to prefix the file name
const image = await page.screenshot();
expect(image).toMatchImageSnapshot({
customSnapshotsDir,
customSnapshotIdentifier: `${context.id}-${process.platform}`,
storeReceivedOnFailure: true,
customReceivedDir,
customDiffDir: customReceivedDir,
});
},
};
export default config;

View File

@@ -70,5 +70,13 @@ module.exports = {
],
},
],
"property-no-deprecated": [
true,
{
ignoreProperties: ["-webkit-box-orient", "word-wrap"],
},
],
"nesting-selector-no-missing-scoping-root": null,
"no-invalid-position-declaration": null,
},
};

View File

@@ -19,3 +19,6 @@ include:
* Thom Cleary (https://github.com/thomcatdotrocks)
Small update for tarball deployment
* Alexander (https://github.com/ioalexander)
Save image on CTRL + S shortcut

View File

@@ -1,3 +1,97 @@
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
====================================================================================================
## 🐛 Bug Fixes
* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
====================================================================================================
## ✨ Features
* Message preview should show tooltip with the full message on hover ([#30265](https://github.com/element-hq/element-web/pull/30265)). Contributed by @MidhunSureshR.
* Support rendering notification badges on platforms that do their own icon overlays ([#30315](https://github.com/element-hq/element-web/pull/30315)). Contributed by @Half-Shot.
* Add SubscriptionViewModel base class ([#30297](https://github.com/element-hq/element-web/pull/30297)). Contributed by @dbkr.
* Enhancement: Save image on CTRL+S ([#30330](https://github.com/element-hq/element-web/pull/30330)). Contributed by @ioalexander.
* Add quote functionality to MessageContextMenu (#29893) ([#30323](https://github.com/element-hq/element-web/pull/30323)). Contributed by @AlirezaMrtz.
* Initial structure for shared component views ([#30216](https://github.com/element-hq/element-web/pull/30216)). Contributed by @dbkr.
## 🐛 Bug Fixes
* [Backport staging] Fix e2e shield being invisible in white mode for encrypted room ([#30411](https://github.com/element-hq/element-web/pull/30411)). Contributed by @RiotRobot.
* Force ED titlebar color for new room list ([#30332](https://github.com/element-hq/element-web/pull/30332)). Contributed by @florianduros.
* Add a background color to left panel for macos titlebar in element desktop ([#30328](https://github.com/element-hq/element-web/pull/30328)). Contributed by @florianduros.
* Fix: Prevent page refresh on Enter key in right panel member search ([#30312](https://github.com/element-hq/element-web/pull/30312)). Contributed by @AlirezaMrtz.
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
====================================================================================================
## ✨ Features
* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
## 🐛 Bug Fixes
* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
====================================================================================================
## ✨ Features
* New room list: add context menu to room list item ([#29952](https://github.com/element-hq/element-web/pull/29952)). Contributed by @florianduros.
* Support for custom message components via Module API ([#30074](https://github.com/element-hq/element-web/pull/30074)). Contributed by @Half-Shot.
* Prompt users to set up recovery ([#30075](https://github.com/element-hq/element-web/pull/30075)). Contributed by @uhoreg.
* Update `IconButton` colors ([#30124](https://github.com/element-hq/element-web/pull/30124)). Contributed by @florianduros.
* New room list: filter list can be collapsed ([#29992](https://github.com/element-hq/element-web/pull/29992)). Contributed by @florianduros.
* Show `EmptyRoomListView` when low priority filter matches zero rooms ([#30122](https://github.com/element-hq/element-web/pull/30122)). Contributed by @MidhunSureshR.
## 🐛 Bug Fixes
* Fix untranslatable string "People" in notifications beta ([#30165](https://github.com/element-hq/element-web/pull/30165)). Contributed by @t3chguy.
* Force verification even after logging in via delegate ([#30141](https://github.com/element-hq/element-web/pull/30141)). Contributed by @andybalaam.
* Hide add integrations button based on UIComponent.AddIntegrations ([#30140](https://github.com/element-hq/element-web/pull/30140)). Contributed by @t3chguy.
* Use nav for new room list and label sections ([#30134](https://github.com/element-hq/element-web/pull/30134)). Contributed by @dbkr.
* Spacestore should emit event after rebuilding home space ([#30132](https://github.com/element-hq/element-web/pull/30132)). Contributed by @MidhunSureshR.
* Handle m.room.pinned\_events being invalid ([#30129](https://github.com/element-hq/element-web/pull/30129)). Contributed by @t3chguy.
Changes in [1.11.104](https://github.com/element-hq/element-web/releases/tag/v1.11.104) (2025-06-17)
====================================================================================================
## ✨ Features
* Update the mobile\_guide page to the new design. ([#30006](https://github.com/element-hq/element-web/pull/30006)). Contributed by @pixlwave.
* Provide a devtool for manually verifying other devices ([#30094](https://github.com/element-hq/element-web/pull/30094)). Contributed by @andybalaam.
* Implement MSC4155: Invite filtering ([#29603](https://github.com/element-hq/element-web/pull/29603)). Contributed by @Half-Shot.
* Add low priority avatar decoration to room tile ([#30065](https://github.com/element-hq/element-web/pull/30065)). Contributed by @MidhunSureshR.
* Add ability to prevent window content being captured by other apps (Desktop) ([#30098](https://github.com/element-hq/element-web/pull/30098)). Contributed by @t3chguy.
* New room list: move message preview in user settings ([#30023](https://github.com/element-hq/element-web/pull/30023)). Contributed by @florianduros.
* New room list: change room options icon ([#30029](https://github.com/element-hq/element-web/pull/30029)). Contributed by @florianduros.
* RoomListStore: Sort low priority rooms to the bottom of the list ([#30070](https://github.com/element-hq/element-web/pull/30070)). Contributed by @MidhunSureshR.
* Add low priority filter pill to the room list UI ([#30060](https://github.com/element-hq/element-web/pull/30060)). Contributed by @MidhunSureshR.
* New room list: remove color gradient in space panel ([#29721](https://github.com/element-hq/element-web/pull/29721)). Contributed by @florianduros.
* /share?msg=foo endpoint using forward message dialog ([#29874](https://github.com/element-hq/element-web/pull/29874)). Contributed by @ara4n.
## 🐛 Bug Fixes
* Do not send empty auth when setting up cross-signing keys ([#29914](https://github.com/element-hq/element-web/pull/29914)). Contributed by @gnieto.
* Settings: flip local video feed by default ([#29501](https://github.com/element-hq/element-web/pull/29501)). Contributed by @jbtrystram.
* AccessSecretStorageDialog: various fixes ([#30093](https://github.com/element-hq/element-web/pull/30093)). Contributed by @richvdh.
* AccessSecretStorageDialog: fix inability to enter recovery key ([#30090](https://github.com/element-hq/element-web/pull/30090)). Contributed by @richvdh.
* Fix failure to upload thumbnail causing image to send as file ([#30086](https://github.com/element-hq/element-web/pull/30086)). Contributed by @t3chguy.
* Low priority menu item should be a toggle ([#30071](https://github.com/element-hq/element-web/pull/30071)). Contributed by @MidhunSureshR.
* Add sanity checks to prevent users from ignoring themselves ([#30079](https://github.com/element-hq/element-web/pull/30079)). Contributed by @MidhunSureshR.
* Fix issue with duplicate images ([#30073](https://github.com/element-hq/element-web/pull/30073)). Contributed by @fatlewis.
* Handle errors returned from Seshat ([#30083](https://github.com/element-hq/element-web/pull/30083)). Contributed by @richvdh.
Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
====================================================================================================
## 🐛 Bug Fixes

View File

@@ -1,7 +1,7 @@
# syntax=docker.io/docker/dockerfile:1.16-labs@sha256:bb5e2b225985193779991f3256d1901a0b3e6a0b284c7bffa0972064f4a6d458
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:f16d8e8af67bb6361231e932b8b3e7afa040cbfed181719a450b02c3821b26c1 AS builder
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:2d63e0f812d023c4c764e83d7e30dc94949304443ebc372d5c445e63a5ae49c1 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:66e34aa81c2faf290ea4e4c28a490f2b35a07478265a2d5994c8637506045eee
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:e61b77b27c8f3124fad6d19e894ca5b603bcaf6a34a2df035511299dfa6fad35
# Need root user to install packages & manipulate the usr directory
USER root

View File

@@ -127,7 +127,6 @@ Unless otherwise specified, the following applies to all code:
2. "Conflicted" typically refers to a getter which wants the same name as the underlying variable.
20. Prefer readonly members over getters backed by a variable, unless an internal setter is required.
21. Prefer Interfaces for object definitions, and types for parameter-value-only declarations.
1. Note that an explicit type is optional if not expected to be used outside of the function call,
unlike in this example:
@@ -161,7 +160,6 @@ Unless otherwise specified, the following applies to all code:
28. Export only what can be reused.
29. Prefer a type like `Optional<X>` (`type Optional<T> = T | null | undefined`) instead
of truly optional parameters.
1. A notable exception is when the likelihood of a bug is minimal, such as when a function
takes an argument that is more often not required than required. An example where the
`?` operator is inappropriate is when taking a room ID: typically the caller should
@@ -260,7 +258,6 @@ Inheriting all the rules of TypeScript, the following additionally apply:
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
if at all possible.
13. A component should only use CSS class names in line with the component name.
1. When knowingly using a class name from another component, document it with a [comment](#comments).
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
@@ -388,7 +385,6 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
properties should be clearly documented.
4. Inside a function, there is no need to comment every line, but consider:
- before a particular multiline section of code within the function, give an overview of what it does,
to make it easier for a reader to follow the flow through the function as a whole.
- if it is anything less than obvious, explain _why_ we are doing a particular operation, with particular emphasis

View File

@@ -20,8 +20,7 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
"https://scalar-staging.vector.im/api"
],
"default_widget_container_height": 280,
"default_country_code": "GB",

8
declaration.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
/*
* 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 "*.module.css";

View File

@@ -109,7 +109,7 @@ yarn test
### End-to-End tests
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
See [`docs/playwright.md`](./docs/playwright.md) for how to run the end-to-end tests.
## General github guidelines

View File

@@ -130,32 +130,37 @@ complete re-branding/private labeling, a more personalised experience can be ach
6. `mobile_builds`: Optional. Like `desktop_builds`, except for the mobile apps. Also described in more detail down below.
7. `mobile_guide_toast`: When `true` (default), users accessing the Element Web instance from a mobile device will be prompted to
download the app instead.
8. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
8. `mobile_guide_app_variant`: Optional. The mobile app that the user is prompted to download from the `/mobile_guide` page. When omitted
the mobile guide will be configured for the new Element X apps. Allowed values are as follows:
1. `element`: Element X Android/iOS.
2. `element-classic`: Element Classic Android/iOS.
3. `element-pro`: Element Pro Android/iOS.
9. `update_base_url`: For the desktop app only, the URL where to acquire update packages. If specified, must be a path to a directory
containing `macos` and `win32` directories, with the update packages within. Defaults to `https://packages.element.io/desktop/update/`
in production.
9. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
configuration found in the well-known location is used instead.
10. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
11. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
10. `map_style_url`: Map tile server style URL for location sharing. e.g. `https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY_GOES_HERE`
This setting is ignored if your homeserver provides `/.well-known/matrix/client` in its well-known location, and the JSON file
at that location has a key `m.tile_server` (or the unstable version `org.matrix.msc3488.tile_server`). In this case, the
configuration found in the well-known location is used instead.
11. `welcome_user_id`: **DEPRECATED** An optional user ID to start a DM with after creating an account. Defaults to nothing (no DM created).
12. `custom_translations_url`: An optional URL to allow overriding of translatable strings. The JSON file must be in a format of
`{"affected|translation|key": {"languageCode": "new string"}}`. See https://github.com/matrix-org/matrix-react-sdk/pull/7886 for details.
12. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
13. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
14. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
13. `branding`: Options for configuring various assets used within the app. Described in more detail down below.
14. `embedded_pages`: Further optional URLs for various assets used within the app. Described in more detail down below.
15. `disable_3pid_login`: When `false` (default), **enables** the options to log in with email address or phone number. Set to
`true` to hide these options.
15. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
16. `disable_login_language_selector`: When `false` (default), **enables** the language selector on the login pages. Set to `true`
to hide this dropdown.
16. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
17. `disable_guests`: When `false` (default), **enable** guest-related functionality (peeking/previewing rooms, etc) for unregistered
users. Set to `true` to disable this functionality.
17. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
18. `user_notice`: Optional notice to show to the user, e.g. for sunsetting a deployment and pushing users to move in their own time.
Takes a configuration object as below:
1. `title`: Required. Title to show at the top of the notice.
2. `description`: Required. The description to use for the notice.
3. `show_once`: Optional. If true then the notice will only be shown once per device.
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
19. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
20. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
21. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
### `desktop_builds` and `mobile_builds`
@@ -445,8 +450,7 @@ If you would like to use Scalar, the integration manager maintained by Element,
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
"https://scalar-staging.vector.im/api"
]
}
```

View File

@@ -55,8 +55,7 @@ Then you can deploy it to your cluster with something like `kubectl apply -f my-
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"defaultCountryCode": "GB",

View File

@@ -15,8 +15,7 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",

View File

@@ -15,8 +15,7 @@
"https://scalar.vector.im/_matrix/integrations/v1",
"https://scalar.vector.im/api",
"https://scalar-staging.vector.im/_matrix/integrations/v1",
"https://scalar-staging.vector.im/api",
"https://scalar-staging.riot.im/scalar/api"
"https://scalar-staging.vector.im/api"
],
"bug_report_endpoint_url": "https://element.io/bugreports/submit",
"uisi_autorageshake_app": "element-auto-uisi",

View File

@@ -17,11 +17,13 @@ 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)"],
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"],
moduleNameMapper: {
// Support CSS module
"\\.(module.css)$": "identity-obj-proxy",
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
"\\.svg$": "<rootDir>/__mocks__/svg.js",
@@ -38,8 +40,6 @@ const config: Config = {
"^!!raw-loader!.*": "jest-raw-loader",
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
// Requires ESM which is incompatible with our current Jest setup
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
},
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
collectCoverageFrom: [

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.103",
"version": "1.11.108",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -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.1.0",
"@playwright/test": "1.52.0",
"@types/react": "19.1.6",
"@types/react-dom": "19.1.6",
"oidc-client-ts": "3.2.1",
"**/pretty-format/react-is": "19.1.1",
"@playwright/test": "1.54.2",
"@types/react": "19.1.9",
"@types/react-dom": "19.1.7",
"oidc-client-ts": "3.3.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001721",
"caniuse-lite": "1.0.30001724",
"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.0.0",
"@element-hq/element-web-module-api": "1.4.1",
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
@@ -89,12 +94,12 @@
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^9.0.0",
"@sentry/browser": "^10.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.11.0",
"@vector-im/matrix-wysiwyg": "2.38.3",
"@vector-im/compound-design-tokens": "^6.0.0",
"@vector-im/compound-web": "^8.1.2",
"@vector-im/matrix-wysiwyg": "2.39.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -112,7 +117,7 @@
"emojibase-regex": "15.3.2",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "10.1.6",
"filesize": "11.0.2",
"github-markdown-css": "^5.5.1",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^11.3.1",
@@ -123,9 +128,9 @@
"jsrsasign": "^11.0.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-react": "4.3.1",
"linkify-string": "4.3.1",
"linkifyjs": "4.3.1",
"linkify-react": "4.3.2",
"linkify-string": "4.3.2",
"linkifyjs": "4.3.2",
"lodash": "^4.17.21",
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
@@ -138,7 +143,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.249.4",
"posthog-js": "1.257.0",
"qrcode": "1.5.4",
"re-resizable": "6.11.2",
"react": "^19.0.0",
@@ -149,6 +154,7 @@
"react-string-replace": "^1.1.1",
"react-transition-group": "^4.4.1",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.12.6",
"rfc4648": "^1.4.0",
"sanitize-filename": "^1.6.3",
"sanitize-html": "2.17.0",
@@ -180,14 +186,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.12.2",
"@element-hq/element-web-playwright-common": "^1.1.5",
"@element-hq/element-call-embedded": "0.14.1",
"@element-hq/element-web-playwright-common": "^1.4.4",
"@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": "^3.0.0",
"@stylistic/eslint-plugin": "^4.0.0",
"@sentry/webpack-plugin": "^4.0.0",
"@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",
"@testing-library/jest-dom": "^6.4.8",
@@ -212,9 +223,9 @@
"@types/node-fetch": "^2.6.2",
"@types/pako": "^2.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "19.1.6",
"@types/react": "19.1.9",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "19.1.6",
"@types/react-dom": "19.1.7",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.16.0",
"@types/semver": "^7.5.8",
@@ -231,10 +242,10 @@
"concurrently": "^9.0.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.38.1",
"cronstrue": "^2.41.0",
"cronstrue": "^3.0.0",
"css-loader": "^7.0.0",
"css-minimizer-webpack-plugin": "^7.0.0",
"dotenv": "^16.0.2",
"dotenv": "^17.0.0",
"eslint": "8.57.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.0.0",
@@ -246,18 +257,20 @@
"eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"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",
"fetch-mock": "9.11.0",
"fetch-mock-jest": "^1.5.1",
"file-loader": "^6.0.0",
"glob": "^11.0.0",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.0.0",
"identity-obj-proxy": "^3.0.0",
"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",
@@ -275,19 +288,20 @@
"postcss-hexrgba": "2.1.0",
"postcss-import": "16.1.0",
"postcss-loader": "8.1.1",
"postcss-mixins": "^11.0.0",
"postcss-mixins": "^12.0.0",
"postcss-nested": "^7.0.0",
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.5.3",
"prettier": "3.6.2",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.13.0",
"stylelint-config-standard": "^38.0.0",
"storybook": "^9.0.12",
"stylelint": "^16.23.0",
"stylelint-config-standard": "^39.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
@@ -295,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

@@ -0,0 +1,46 @@
diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
index 498bb69..4e89216 100644
--- a/node_modules/@types/mdx/types.d.ts
+++ b/node_modules/@types/mdx/types.d.ts
@@ -5,7 +5,7 @@
*/
// @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
// defined or not.
-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
+type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
/**
* This matches any function component types that ar part of `ElementType`.
@@ -20,12 +20,12 @@ type ClassElementType = Extract<ElementType, new(props: Record<string, any>) =>
/**
* A valid JSX string component.
*/
-type StringComponent = Extract<keyof JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
+type StringComponent = Extract<keyof React.JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
/**
* A JSX element returned by MDX content.
*/
-export type Element = JSX.Element;
+export type Element = React.JSX.Element;
/**
* A valid JSX function component.
@@ -44,7 +44,7 @@ type FunctionComponent<Props> = ElementType extends never
*/
type ClassComponent<Props> = ElementType extends never
// If JSX.ElementType isnt defined, the valid return type is a constructor that returns JSX.ElementClass
- ? new(props: Props) => JSX.ElementClass
+ ? new(props: Props) => React.JSX.ElementClass
: ClassElementType extends never
// If JSX.ElementType is defined, but doesnt allow constructors, function components are disallowed.
? never
@@ -70,7 +70,7 @@ interface NestedMDXComponents {
export type MDXComponents =
& NestedMDXComponents
& {
- [Key in StringComponent]?: Component<JSX.IntrinsicElements[Key]>;
+ [Key in StringComponent]?: Component<React.JSX.IntrinsicElements[Key]>;
}
& {
/**

View File

@@ -1,31 +0,0 @@
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index d3318dc..c2b2c77 100644
--- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts
@@ -134,7 +134,7 @@ declare namespace React {
props: P,
) => ReactNode | Promise<ReactNode>)
// constructor signature must match React.Component
- | (new(props: P) => Component<any, any>);
+ | (new(props: P, context?: any) => Component<any, any>);
/**
* Created by {@link createRef}, or {@link useRef} when passed `null`.
@@ -945,7 +945,7 @@ declare namespace React {
context: unknown;
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
- constructor(props: P);
+ constructor(props: P, context?: unknown);
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
@@ -1117,7 +1117,7 @@ declare namespace React {
*/
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
// constructor signature must match React.Component
- new(props: P): Component<P, S>;
+ new(props: P, context?: any): Component<P, S>;
/**
* Ignored by React.
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.

View File

@@ -19,6 +19,7 @@ const clickButtonReply = async (tile: Locator) => {
await tile.hover();
await tile.getByRole("button", { name: "Reply", exact: true }).click();
}).toPass();
await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible();
};
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
@@ -39,7 +40,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// wait for the tile to finish loading
await expect(
page
.locator(".mx_AudioPlayer_mediaName")
.getByTestId("audio-player-name")
.last()
.filter({ hasText: file.split("/").at(-1) }),
).toBeVisible();
@@ -54,12 +55,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// Check that the audio player is rendered and its button becomes visible
const checkPlayerVisibility = async (locator: Locator) => {
// Assert that the audio player and media information are visible
const mediaInfo = locator.locator(
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo",
);
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
const mediaInfo = locator.getByRole("region", { name: "Audio player" });
await expect(mediaInfo.getByText(".ogg")).toBeVisible(); // extension
await expect(mediaInfo.getByRole("time")).toHaveText("00:01"); // duration
await expect(mediaInfo.getByText("(3.56 KB)")).toBeVisible(); // actual size;
// Assert that the play button can be found and is visible
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
@@ -78,7 +77,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
}
// Check the status of the seek bar
expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0);
expect(await page.getByRole("region", { name: "Audio player" }).getByRole("slider").count()).toBeGreaterThan(0);
// Enable IRC layout
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
@@ -100,7 +99,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
display: none !important;
}
`,
mask: [page.locator(".mx_AudioPlayer_seek")],
mask: [page.getByTestId("audio-player-seek")],
};
// Take a snapshot of mx_EventTile_last on IRC layout
@@ -186,9 +185,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert that the audio player is rendered
const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
// Assert that the counter is zero before clicking the play button
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(container.getByRole("timer")).toHaveText("00:00");
// Find and click "Play" button, the wait is to make the test less flaky
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
@@ -198,7 +197,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
// Assert that the timer is reset when the audio file finished playing
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(container.getByRole("timer")).toHaveText("00:00");
// Assert that "Play" button can be found
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
@@ -226,7 +225,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
// Find and click "Reply" button on MessageActionBar
const tile = page.locator(".mx_EventTile_last");
@@ -236,7 +235,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/1sec.ogg");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
// Assert that replied audio file is rendered as file button inside ReplyChain
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
@@ -261,7 +260,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await expect(
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
).toBeVisible();
await clickButtonReply(tile);
@@ -269,7 +270,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
// Assert that the audio player is rendered
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await expect(
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
).toBeVisible();
await clickButtonReply(tile);
@@ -277,7 +280,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
// Assert that the audio player is rendered
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
// Assert that there are two "mx_ReplyChain" elements
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
@@ -313,7 +316,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// On the main timeline
const messageList = page.locator(".mx_RoomView_MessageList");
// Assert the audio player is rendered
await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
await expect(
messageList.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
).toBeVisible();
// Find and click "Reply in thread" button
await messageList.locator(".mx_EventTile_last").hover();
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
@@ -321,10 +326,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
// On a thread
const thread = page.locator(".mx_ThreadView");
const threadTile = thread.locator(".mx_EventTile_last");
const audioPlayer = threadTile.locator(".mx_AudioPlayer_container");
const audioPlayer = threadTile.getByRole("region", { name: "Audio player" });
// Assert that the counter is zero before clicking the play button
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
// Find and click "Play" button, the wait is to make the test less flaky
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
@@ -334,7 +339,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
// Assert that the timer is reset when the audio file finished playing
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
// Assert that "Play" button can be found
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();

View File

@@ -28,7 +28,7 @@ test.describe("Composer", () => {
test.describe("CIDER", () => {
test("sends a message when you click send or press Enter", async ({ page }) => {
const composer = page.getByRole("textbox", { name: "Send a message…" });
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
// Type a message
await composer.pressSequentially("my message 0");
@@ -52,7 +52,7 @@ test.describe("Composer", () => {
});
test("can write formatted text", async ({ page }) => {
const composer = page.getByRole("textbox", { name: "Send a message…" });
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await composer.pressSequentially("my bold");
await composer.press(`${CtrlOrMeta}+KeyB`);
@@ -68,7 +68,7 @@ test.describe("Composer", () => {
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
});
@@ -79,7 +79,7 @@ test.describe("Composer", () => {
});
test("only sends when you press Control+Enter", async ({ page }) => {
const composer = page.getByRole("textbox", { name: "Send a message…" });
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
// Type a message and press Enter
await composer.pressSequentially("my message 3");
await composer.press("Enter");

View File

@@ -91,10 +91,10 @@ test.describe("Key backup reset from elsewhere", () => {
await csAPI.deleteBackupVersion(backupInfo.version);
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
await page.getByRole("textbox", { name: "Send a message…" }).fill("/discardsession");
await page.getByRole("button", { name: "Send message" }).click();
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
await page.getByRole("textbox", { name: "Send a message…" }).fill("Message with broken key backup");
await page.getByRole("button", { name: "Send message" }).click();
// Should be the message we sent plus the room creation event

View File

@@ -154,10 +154,12 @@ test.describe("Cryptography", function () {
await app.client.bootstrapCrossSigning(aliceCredentials);
await startDMWithBob(page, bob);
// send first message
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
await checkDMRoom(page);
const bobRoomId = await bobJoin(page, bob);
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png");
await testMessages(page, bob, bobRoomId);
await verify(app, bob);
@@ -168,6 +170,7 @@ test.describe("Cryptography", function () {
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon.png");
},
);

View File

@@ -48,31 +48,38 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
return promiseVerificationRequest;
}
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
test(
"Verify device with SAS during login",
{ tag: "@screenshot" },
async ({ page, app, credentials, homeserver }) => {
await logIntoElement(page, credentials);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
// Launch the verification request between alice and the bot
const verificationRequest = await initiateAliceVerificationRequest(page);
// Handle emoji SAS verification
const infoDialog = page.locator(".mx_InfoDialog");
// the bot chooses to do an emoji verification
const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1"));
// Handle emoji SAS verification
const infoDialog = page.locator(".mx_InfoDialog");
// the bot chooses to do an emoji verification
const verifier = await verificationRequest.evaluateHandle((request) =>
request.startVerification("m.sas.v1"),
);
// Handle emoji request and check that emojis are matching
await doTwoWaySasVerification(page, verifier);
// Handle emoji request and check that emojis are matching
await doTwoWaySasVerification(page, verifier);
await infoDialog.getByRole("button", { name: "They match" }).click();
await infoDialog.getByRole("button", { name: "Got it" }).click();
await infoDialog.getByRole("button", { name: "They match" }).click();
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
await infoDialog.getByRole("button", { name: "Got it" }).click();
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that our device is now cross-signed
await checkDeviceIsCrossSigned(app);
// Check that the current device is connected to key backup
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
});
// Check that the current device is connected to key backup
// For now we don't check that the backup key is in cache because it's a bit flaky,
// as we need to wait for the secret gossiping to happen.
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
},
);
// Regression test for https://github.com/element-hq/element-web/issues/29110
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
@@ -117,6 +124,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
const toasts = new Toasts(page);
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
// There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause
// a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes.
await page.unrouteAll({ behavior: "ignoreErrors" });
});
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
@@ -198,7 +209,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
const dialog = page.locator(".mx_Dialog");
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
// (cf https://github.com/element-hq/element-web/issues/30089)
await dialog.locator("textarea").pressSequentially(recoveryKey);
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click();

View File

@@ -228,7 +228,7 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
await useSecurityKey.click();
}
// Fill in the recovery key
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
await page.getByRole("button", { name: "Continue", disabled: false }).click();
await page.getByRole("button", { name: "Done" }).click();
}
@@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) {
const settings = await app.settings.openUserSettings("Encryption");
await settings.getByRole("button", { name: "Verify this device" }).click();
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
await app.page.getByRole("button", { name: "Done" }).click();
await app.settings.closeDialog();

View File

@@ -30,6 +30,10 @@ test.describe("Lazy Loading", () => {
});
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
// The charlies were running off the bottom of the screen.
// We no longer overscan the member list so the result is they are not in the dom.
// Increase the viewport size to ensure they are.
await page.setViewportSize({ width: 1000, height: 1000 });
for (let i = 1; i <= 10; i++) {
const displayName = `Charly #${i}`;
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });

View File

@@ -22,13 +22,21 @@ test.describe("Room list filters and sort", () => {
});
function getPrimaryFilters(page: Page): Locator {
return page.getByRole("listbox", { name: "Room list filters" });
return page.getByTestId("primary-filters");
}
function getRoomOptionsMenu(page: Page): Locator {
return page.getByRole("button", { name: "Room Options" });
}
function getFilterExpandButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Expand filter list" });
}
function getFilterCollapseButton(page: Page): Locator {
return getPrimaryFilters(page).getByRole("button", { name: "Collapse filter list" });
}
/**
* Get the room list
* @param page
@@ -136,6 +144,7 @@ test.describe("Room list filters and sort", () => {
await tile.click();
// Enable Favourite filter
await getFilterExpandButton(page).click();
const primaryFilters = getPrimaryFilters(page);
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(tile).not.toBeVisible();
@@ -220,17 +229,13 @@ test.describe("Room list filters and sort", () => {
// only one room should be visible
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(4);
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(4);
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "People" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(2);
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(2);
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
@@ -238,15 +243,24 @@ test.describe("Room list filters and sort", () => {
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(5);
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(5);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Invites" }).click();
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await expect.poll(() => roomList.locator("role=gridcell").count()).toBe(1);
await getFilterCollapseButton(page).click();
await expect(primaryFilters.locator("role=option").first()).toHaveText("Invites");
});
test(
@@ -312,7 +326,9 @@ test.describe("Room list filters and sort", () => {
async ({ page, app, user }) => {
const emptyRoomList = getEmptyRoomList(page);
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
await expect(page.getByRole("navigation", { name: "Room list" })).toMatchScreenshot(
"room-panel-empty-room-list.png",
);
},
);
@@ -326,6 +342,8 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);
@@ -343,6 +361,8 @@ test.describe("Room list filters and sort", () => {
{ tag: "@screenshot" },
async ({ page, app, user }) => {
const primaryFilters = getPrimaryFilters(page);
await getFilterExpandButton(page).click();
await primaryFilters.getByRole("option", { name: filter }).click();
const emptyRoomList = getEmptyRoomList(page);

View File

@@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => {
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
// New message should open the direct messages dialog
await page.getByRole("menuitem", { name: "New message" }).click();
// Start chat should open the direct messages dialog
await page.getByRole("menuitem", { name: "Start chat" }).click();
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
await app.closeDialog();

View File

@@ -19,7 +19,7 @@ test.describe("Room list panel", () => {
* @param page
*/
function getRoomListView(page: Page) {
return page.getByTestId("room-list-panel");
return page.getByRole("navigation", { name: "Room list" });
}
test.beforeEach(async ({ page, app, user }) => {
@@ -44,7 +44,7 @@ test.describe("Room list panel", () => {
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
await page.setViewportSize({ width: 575, height: 600 });
const roomListPanel = page.getByTestId("room-list-panel");
const roomListPanel = getRoomListView(page);
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
});
});

View File

@@ -49,8 +49,7 @@ test.describe("Room list", () => {
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
@@ -60,6 +59,12 @@ test.describe("Room list", () => {
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
});
test("should open the context menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click({ button: "right" });
await expect(page.getByRole("menu", { name: "More Options" })).toBeVisible();
});
test("should open the more options menu", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
const roomItem = roomListView.getByRole("gridcell", { name: "Open room room29" });
@@ -109,10 +114,13 @@ test.describe("Room list", () => {
// It should make the room muted
await page.getByRole("menuitem", { name: "Mute room" }).click();
await expect(roomItem.getByTestId("notification-decoration")).not.toBeVisible();
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
// The room decoration should have the muted icon
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
@@ -133,8 +141,9 @@ test.describe("Room list", () => {
// Put focus on the room list
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
// Scroll to the end of the room list
await page.mouse.wheel(0, 1000);
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
const filters = page.getByRole("listbox", { name: "Room list filters" });
@@ -223,17 +232,17 @@ test.describe("Room list", () => {
await expect(notificationButton).toBeFocused();
// Open the menu
await notificationButton.click();
await page.keyboard.press("Enter");
// Wait for the menu to be open
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
"aria-selected",
"true",
);
// Close the menu
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Escape");
// Focus should be back on the room list item
await expect(room29).toBeFocused();
// Focus should be back on the notification button
await expect(notificationButton).toBeFocused();
});
});
});
@@ -278,7 +287,7 @@ test.describe("Room list", () => {
});
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
await page.getByRole("navigation", { name: "Room list" }).getByRole("button", { name: "Add" }).click();
await page.getByRole("menuitem", { name: "New video room" }).click();
await page.getByRole("textbox", { name: "Name" }).fill("video room");
await page.getByRole("button", { name: "Create video room" }).click();

View File

@@ -13,7 +13,7 @@ import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";
async function sendMessage(page: Page, message: string): Promise<Locator> {
await page.getByRole("textbox", { name: "Send a message…" }).fill(message);
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill(message);
await page.getByRole("button", { name: "Send message" }).click();
const msgTile = page.locator(".mx_EventTile_last");
@@ -22,7 +22,7 @@ async function sendMessage(page: Page, message: string): Promise<Locator> {
}
async function sendMultilineMessages(page: Page, messages: string[]) {
await page.getByRole("textbox", { name: "Send a message…" }).focus();
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).focus();
for (let i = 0; i < messages.length; i++) {
await page.keyboard.type(messages[i]);
if (i < messages.length - 1) await page.keyboard.press("Shift+Enter");
@@ -40,7 +40,7 @@ async function replyMessage(page: Page, message: Locator, replyMessage: string):
await line.hover();
await line.getByRole("button", { name: "Reply", exact: true }).click();
await page.getByRole("textbox", { name: "Send a reply…" }).fill(replyMessage);
await page.getByRole("textbox", { name: "Send an unencrypted reply…" }).fill(replyMessage);
await page.getByRole("button", { name: "Send message" }).click();
const msgTile = page.locator(".mx_EventTile_last");

View File

@@ -0,0 +1,36 @@
/*
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, expect } from "../../element-web-test";
import { MobileAppVariant } from "../../../src/vector/mobile_guide/mobile-apps";
const variants = [MobileAppVariant.Classic, MobileAppVariant.X, MobileAppVariant.Pro];
test.describe("Mobile Guide Screenshots", { tag: "@screenshot" }, () => {
for (const variant of variants) {
test.describe(`for variant ${variant}`, () => {
test.use({
config: {
default_server_config: {
"m.homeserver": {
base_url: "https://matrix.server.invalid",
server_name: "server.invalid",
},
},
mobile_guide_app_variant: variant,
},
viewport: { width: 390, height: 844 }, // iPhone 16e
});
test("should match the mobile_guide screenshot", async ({ page, axe }) => {
await page.goto("/mobile_guide/");
await expect(page).toMatchScreenshot(`mobile-guide-${variant}.png`);
await expect(axe).toHaveNoViolations();
});
});
}
});

View File

@@ -0,0 +1,160 @@
/*
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 Page } from "@playwright/test";
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_JumpToBottomButton {
display: none !important;
}
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
});
const IMAGE_FILE = fs.readFileSync("playwright/sample-files/element.png");
test.describe("Custom Component API", () => {
test.use({
displayName: "Manny",
config: {
modules: ["/modules/custom-component-module.js"],
},
page: async ({ page }, use) => {
await page.route("/modules/custom-component-module.js", async (route) => {
await route.fulfill({ path: "playwright/sample-files/custom-component-module.js" });
});
await use(page);
},
room: async ({ page, app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "TestRoom" });
await use({ roomId });
},
});
test.describe("basic functionality", () => {
test(
"should replace the render method of a textual event",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Simple message");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile.png",
screenshotOptions(page),
);
},
);
test(
"should fall through if one module does not render a component",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Fall through here");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-fall-through.png",
screenshotOptions(page),
);
},
);
test(
"should render the original content of a textual event conditionally",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not replace me");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-tile-original.png",
screenshotOptions(page),
);
},
);
test("should disallow editing when the allowEditingEvent hint is set to false", async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Do not show edits");
await page.getByText("Do not show edits").hover();
await expect(
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }),
).not.toBeVisible();
});
test("should disallow downloading media when the allowDownloading hint is set to false", async ({
page,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "bad.png", type: "image/png" });
await app.client.sendEvent(room.roomId, null, "m.room.message", {
msgtype: "m.image",
body: "bad.png",
url: upload.content_uri,
});
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await expect(page.getByRole("button", { name: "Download" })).not.toBeVisible();
await imgTile.click();
await expect(page.getByLabel("Image view").getByLabel("Download")).not.toBeVisible();
});
test("should allow downloading media when the allowDownloading hint is set to true", async ({
page,
room,
app,
}) => {
await app.viewRoomById(room.roomId);
await app.viewRoomById(room.roomId);
const upload = await app.client.uploadContent(IMAGE_FILE, { name: "good.png", type: "image/png" });
await app.client.sendEvent(room.roomId, null, "m.room.message", {
msgtype: "m.image",
body: "good.png",
url: upload.content_uri,
});
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await expect(page.getByRole("button", { name: "Download" })).toBeVisible();
await imgTile.click();
await expect(page.getByLabel("Image view").getByLabel("Download")).toBeVisible();
});
test(
"should render the next registered component if the filter function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the filter!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-filter.png",
screenshotOptions(page),
);
},
);
test(
"should render original component if the render function throws",
{ tag: "@screenshot" },
async ({ page, room, app }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, "Crash the renderer!");
await expect(await page.locator(".mx_EventTile_last")).toMatchScreenshot(
"custom-component-crash-handle-renderer.png",
screenshotOptions(page),
);
},
);
});
});

View File

@@ -11,6 +11,9 @@ import { type Page } from "@playwright/test";
import { expect } from "../../element-web-test";
/**
* Click through registering a new user in the MAS UI.
*/
export async function registerAccountMas(
page: Page,
mailpit: MailpitClient,
@@ -42,3 +45,17 @@ export async function registerAccountMas(
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}
/**
* Click through entering username and password into the MAS login prompt.
*/
export async function logInAccountMas(page: Page, username: string, password: string): Promise<void> {
await expect(page.getByText("Please sign in to continue:")).toBeVisible();
await page.getByRole("textbox", { name: "Username" }).fill(username);
await page.getByRole("textbox", { name: "Password", exact: true }).fill(password);
await page.getByRole("button", { name: "Continue" }).click();
await expect(page.getByText("Allow access to your account?")).toBeVisible();
await page.getByRole("button", { name: "Continue" }).click();
}

View File

@@ -6,8 +6,12 @@ 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, 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 { test, expect } from "../../element-web-test.ts";
import { registerAccountMas } from ".";
import { logInAccountMas, registerAccountMas } from ".";
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
@@ -77,7 +81,7 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
test(
"it should log out the user & wipe data when logging out via MAS",
{ tag: "@screenshot" },
async ({ mas, page, mailpitClient }, testInfo) => {
async ({ mas, page, mailpitClient, homeserver }, testInfo) => {
// We use this over the `user` fixture to ensure we get an OIDC session rather than a compatibility one
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
@@ -91,14 +95,168 @@ test.describe("OIDC Native", { tag: ["@no-firefox", "@no-webkit"] }, () => {
const result = await mas.manage("kill-sessions", userId);
expect(result.output).toContain("Ended 1 active OAuth 2.0 session");
// Workaround for Synapse's 2 minute cache on MAS token validity
// (https://github.com/element-hq/synapse/pull/18231)
await homeserver.restart();
await page.goto("http://localhost:8080");
await expect(
page.getByText("For security, this session has been signed out. Please sign in again."),
).toBeVisible();
await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
//await expect(page).toMatchScreenshot("token-expired.png", { includeDialogBackground: true });
const localStorageKeys = await page.evaluate(() => Object.keys(localStorage));
expect(localStorageKeys).toHaveLength(0);
},
);
test("can log in to an existing MAS account", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
// Allow the outstanding requests queue to settle before logging out
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be in (we see an error because we have no recovery key).
await expect(page.getByText("Unable to verify this device")).toBeVisible();
});
test.describe("with force_verification on", () => {
test.use({
config: {
force_verification: true,
},
});
test("verify dialog cannot be dismissed", { tag: "@screenshot" }, async ({ page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, "Pa$sW0rD!");
await expect(page.getByText("Welcome")).toBeVisible();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be being warned that we need to verify (but we can't)
await expect(page.getByText("Unable to verify this device")).toBeVisible();
// And there should be no way to close this prompt
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
});
test(
"continues to show verification prompt after cancelling device verification",
{ tag: "@screenshot" },
async ({ browser, config, homeserver, page, mailpitClient }, testInfo) => {
// Register an account with MAS
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
const userId = `alice_${testInfo.testId}`;
const password = "Pa$sW0rD!";
await registerAccountMas(page, mailpitClient, userId, `${userId}@email.com`, password);
await expect(page.getByText("Welcome")).toBeVisible();
// Log in an additional account, and verify it.
//
// This means that when we log out and in again, we are offered
// to verify using another device.
const otherContext = await newContext(browser, config, homeserver);
const otherDevicePage = await otherContext.newPage();
await otherDevicePage.goto("/#/login");
await otherDevicePage.getByRole("button", { name: "Continue" }).click();
await logInAccountMas(otherDevicePage, userId, password);
await verifyUsingOtherDevice(otherDevicePage, page);
await otherDevicePage.close();
// Log out
await page.getByRole("button", { name: "User menu" }).click();
await expect(page.getByText(userId, { exact: true })).toBeVisible();
await page.waitForTimeout(2000);
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
await expect(page).toHaveURL(/\/#\/login$/);
// Log in again
await page.goto("/#/login");
await page.getByRole("button", { name: "Continue" }).click();
await page.getByRole("button", { name: "Continue" }).click();
// We should be in, and not able to dismiss the verify dialog
await expect(page.getByText("Verify this device")).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
// When we start verifying with another device
await page.getByRole("button", { name: "Verify with another device" }).click();
// And then cancel it
await page.getByRole("button", { name: "Close dialog" }).click();
// Then we should still be at the unskippable verify prompt
await expect(page.getByText("Verify this device")).toBeVisible();
await expect(page.getByRole("button", { name: "Skip verification for now" })).not.toBeVisible();
},
);
});
});
/**
* Perform interactive emoji verification for a new device.
*/
async function verifyUsingOtherDevice(deviceToVerifyPage: Page, alreadyVerifiedDevicePage: Page) {
await deviceToVerifyPage.getByRole("button", { name: "Verify with another device" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Verify session" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Start" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "They match" }).click();
await deviceToVerifyPage.getByRole("button", { name: "They match" }).click();
await alreadyVerifiedDevicePage.getByRole("button", { name: "Got it" }).click();
await deviceToVerifyPage.getByRole("button", { name: "Got it" }).click();
}
/**
* Create a new browser context which serves up the default config plus what you supplied, and sets m.homeserver to the
* supplied homeserver's URL.
*/
async function newContext(browser: Browser, config: Partial<Partial<Config>>, homeserver: StartedHomeserverContainer) {
const otherContext = await browser.newContext();
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

@@ -29,7 +29,7 @@ test.describe("Pills", () => {
// send a message using the built-in room mention functionality (autocomplete)
await page
.getByRole("textbox", { name: "Send a message…" })
.getByRole("textbox", { name: "Send an unencrypted message…" })
.pressSequentially(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
await page.locator(".mx_Autocomplete_Completion_title").click();
await page.getByRole("button", { name: "Send message" }).click();

View File

@@ -42,7 +42,10 @@ export class Helpers {
*/
async assertReleaseAnnouncementIsVisible(name: string) {
await expect(this.getReleaseAnnouncement(name)).toBeVisible();
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, { showTooltips: true });
await expect(this.page).toMatchScreenshot(`release-announcement-${name}.png`, {
showTooltips: true,
hideJumpToBottomButton: true,
});
}
/**

View File

@@ -63,9 +63,7 @@ test.describe("FilePanel", () => {
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible();
// Assert that the audio player is rendered
await expect(
roomViewBody.locator(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container"),
).toBeVisible();
await expect(roomViewBody.getByRole("region", { name: "Audio player" })).toBeVisible();
// Assert that the file button exists
await expect(
@@ -97,9 +95,7 @@ test.describe("FilePanel", () => {
await expect(image.locator("img[alt='riot.png']")).toBeVisible();
// Detect the audio file
const audio = filePanelMessageList.locator(
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
);
const audio = filePanelMessageList.getByRole("region", { name: "Audio player" });
// Assert that the play button is rendered
await expect(audio.getByRole("button", { name: "Play" })).toBeVisible();
@@ -130,7 +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.locator(".mx_MessageTimestamp, .mx_AudioPlayer_seek")],
mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")],
});
});
@@ -138,21 +134,19 @@ test.describe("FilePanel", () => {
// Upload an image file
await uploadFile(page, "playwright/sample-files/1sec.ogg");
const audioBody = page.locator(
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
);
const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" });
// Assert that the audio player is rendered
// Assert that the audio file information is rendered
const mediaInfo = audioBody.locator(".mx_AudioPlayer_mediaInfo");
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName").getByText("1sec.ogg")).toBeVisible();
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
// Assert that the audio file information is rendered;
await expect(audioBody.getByText("1sec.ogg")).toBeVisible(); // extension
await expect(audioBody.getByRole("time")).toHaveText("00:01"); // duration
await expect(audioBody.getByText("(3.56 KB)")).toBeVisible(); // actual size;
// Assert that the duration counter is 00:01 before clicking the play button
await expect(audioBody.locator(".mx_AudioPlayer_mediaInfo time", { hasText: "00:01" })).toBeVisible();
await expect(audioBody.getByRole("time")).toHaveText("00:01");
// Assert that the counter is zero before clicking the play button
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
// Click the play button
await audioBody.getByRole("button", { name: "Play" }).click();
@@ -161,7 +155,7 @@ test.describe("FilePanel", () => {
await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible();
// Assert that the timer is reset when the audio file finished playing
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
// Assert that the play button is rendered
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();

View File

@@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot";
const ROOM_NAME = "Test room";
const NAME = "Alice";
async function setupRoomWithMembers(
app: any,
page: any,
homeserver: any,
roomName: string,
memberNames: string[],
): Promise<string> {
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
const id = await app.client.createRoom({ name: roomName, visibility });
const bots: Bot[] = [];
for (let i = 0; i < memberNames.length; i++) {
const displayName = memberNames[i];
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
if (displayName === "Susan") {
await bot.prepareClient();
await app.client.inviteUser(id, bot.credentials?.userId);
} else {
await bot.joinRoom(id);
}
bots.push(bot);
}
return id;
}
test.use({
synapseConfig: {
presence: {
@@ -25,17 +51,8 @@ test.use({
test.describe("Memberlist", () => {
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
testInfo.setTimeout(testInfo.timeout + 30_000);
const id = await app.client.createRoom({ name: ROOM_NAME });
const newBots: Bot[] = [];
const names = ["Bob", "Bob", "Susan"];
for (let i = 0; i < 3; i++) {
const displayName = names[i];
const autoAcceptInvites = displayName !== "Susan";
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
await bot.prepareClient();
await app.client.inviteUser(id, bot.credentials?.userId);
newBots.push(bot);
}
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
});
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
@@ -45,4 +62,37 @@ test.describe("Memberlist", () => {
await expect(memberlist.getByText("Invited")).toHaveCount(1);
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
});
test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => {
// Create a room with many members to enable scrolling
const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`);
await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames);
// Navigate to the room and open member list
await app.viewRoomByName("Large Room");
const memberlist = await app.toggleMemberlistPanel();
// Get the scrollable container
const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar");
// Scroll down to the bottom of the member list
await app.scrollListToBottom(memberListContainer);
// Wait for the target member to be visible after scrolling
const targetName = "Member14";
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
await targetMember.waitFor({ state: "visible" });
// Verify Alice is not visible at this point
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
// Click on a member near the bottom of the list
await expect(targetMember).toBeVisible();
await targetMember.click();
// Verify that the user info screen is shown and hasn't scrolled back to top
await expect(page.locator(".mx_UserInfo")).toBeVisible();
await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible();
});
});

View File

@@ -35,7 +35,7 @@ test.describe("Share dialog", () => {
const rightPanel = await app.toggleRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
await rightPanel.getByRole("option", { name: user.displayName }).click();
await rightPanel.getByRole("button", { name: "Share profile" }).click();
const dialog = page.getByRole("dialog", { name: "Share User" });

View File

@@ -23,7 +23,7 @@ async function openSpaceContextMenu(page: Page, app: ElementAppPage, spaceName:
return page.locator(".mx_SpacePanel_contextMenu");
}
function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
function spaceCreateOptions(serverName: string, spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
return {
creation_content: {
type: "m.space",
@@ -35,17 +35,21 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
name: spaceName,
},
},
...roomIds.map((r) => spaceChildInitialState(r)),
...roomIds.map((r) => spaceChildInitialState(serverName, r)),
],
};
}
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
function spaceChildInitialState(
serverName: string,
roomId: string,
order?: string,
): ICreateRoomOpts["initial_state"]["0"] {
return {
type: "m.space.child",
state_key: roomId,
content: {
via: [roomId.split(":")[1]],
via: [serverName],
order,
},
};
@@ -240,7 +244,7 @@ test.describe("Spaces", () => {
});
await expect(await app.getSpacePanelButton("My Space")).toBeVisible();
const roomId = await bot.createRoom(spaceCreateOptions("Space Space"));
const roomId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Space Space"));
await bot.inviteUser(roomId, user.userId);
// Assert that `Space Space` is above `My Space` due to it being an invite
@@ -260,7 +264,10 @@ test.describe("Spaces", () => {
const spaceName = "Spacey Mc. Space Space";
await app.client.createSpace({
name: spaceName,
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
initial_state: [
spaceChildInitialState(user.homeServer, roomId1),
spaceChildInitialState(user.homeServer, roomId2),
],
});
await app.viewSpaceHomeByName(spaceName);
@@ -287,7 +294,7 @@ test.describe("Spaces", () => {
});
await app.client.createSpace({
name: "Root Space",
initial_state: [spaceChildInitialState(childSpaceId)],
initial_state: [spaceChildInitialState(user.homeServer, childSpaceId)],
});
// Find collapsed Space panel
@@ -323,7 +330,7 @@ test.describe("Spaces", () => {
name: "Test Room",
topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
});
const spaceId = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
const spaceId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Test Space", [roomId]));
await bot.inviteUser(spaceId, user.userId);
await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
@@ -361,9 +368,9 @@ test.describe("Spaces", () => {
await app.client.createSpace({
name: "Root Space",
initial_state: [
spaceChildInitialState(childSpaceId1, "a"),
spaceChildInitialState(childSpaceId2, "b"),
spaceChildInitialState(childSpaceId3, "c"),
spaceChildInitialState(user.homeServer, childSpaceId1, "a"),
spaceChildInitialState(user.homeServer, childSpaceId2, "b"),
spaceChildInitialState(user.homeServer, childSpaceId3, "c"),
],
});
await app.viewSpaceByName("Root Space");

View File

@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
await result.first().click();
// send first message to start DM
const locator = page.getByRole("textbox", { name: "Send a message…" });
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await expect(locator).toBeFocused();
await locator.fill("Hey!");
await locator.press("Enter");
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
// Send first message to actually start DM
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
const locator = page.getByRole("textbox", { name: "Send a message…" });
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
await locator.fill("Hey!");
await locator.press("Enter");

View File

@@ -43,7 +43,7 @@ test.describe("Threads", () => {
const roomViewLocator = page.locator(".mx_RoomView_body");
// User sends message
const textbox = roomViewLocator.getByRole("textbox", { name: "Send a message…" });
const textbox = roomViewLocator.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill("Hello Mr. Bot");
await textbox.press("Enter");
@@ -108,7 +108,7 @@ test.describe("Threads", () => {
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
// User responds in thread
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" });
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" });
await locator.fill("Test");
await locator.press("Enter");
@@ -262,7 +262,7 @@ test.describe("Threads", () => {
await locator.locator(".mx_EventTile_line").click();
// User responds & asserts
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" });
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" });
await locator.fill("Great!");
await locator.press("Enter");
@@ -335,8 +335,8 @@ test.describe("Threads", () => {
// Send message
const locator = page.locator(".mx_RoomView_body");
await locator.getByRole("textbox", { name: "Send a message…" }).fill("Hello Mr. Bot");
await locator.getByRole("textbox", { name: "Send a message…" }).press("Enter");
await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hello Mr. Bot");
await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
// Create thread
const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" });
await locator2.hover();
@@ -366,7 +366,7 @@ test.describe("Threads", () => {
let locator = page.locator(".mx_RoomView_body");
// User sends message
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill("Hello Mr. Bot");
await textbox.press("Enter");
// Wait for message to send, get its ID and save as @threadId
@@ -395,7 +395,7 @@ test.describe("Threads", () => {
locator = page.locator(".mx_ThreadView");
await locator.locator(".mx_EventTile_last").hover();
await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click();
textbox = locator.getByRole("textbox", { name: "Reply to thread…" });
textbox = locator.getByRole("textbox", { name: "Reply to unencrypted thread…" });
await textbox.fill("Please come here");
await textbox.press("Enter");
// Wait until the reply is sent
@@ -414,7 +414,7 @@ test.describe("Threads", () => {
// Send message
let locator = page.locator(".mx_RoomView_body");
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill("Hello Mr. Bot");
await textbox.press("Enter");
// Create thread
@@ -425,7 +425,7 @@ test.describe("Threads", () => {
// Send message to thread
locator = page.locator(".mx_ThreadPanel");
textbox = locator.getByRole("textbox", { name: "Send a message…" });
textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill("Hello Mr. User");
await textbox.press("Enter");
await expect(locator.locator(".mx_EventTile_last").getByText("Hello Mr. User")).toBeAttached();
@@ -456,7 +456,7 @@ test.describe("Threads", () => {
*/
const sendMessage = async (message: string) => {
const messageComposer = page.getByRole("region", { name: "Message composer" });
const textbox = messageComposer.getByRole("textbox", { name: "Send a message…" });
const textbox = messageComposer.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill(message);
await textbox.press("Enter");
};
@@ -478,7 +478,7 @@ test.describe("Threads", () => {
// Send a message in the thread
const threadPanel = page.locator(".mx_ThreadPanel");
const textbox = threadPanel.getByRole("textbox", { name: "Send a message…" });
const textbox = threadPanel.getByRole("textbox", { name: "Send an unencrypted message…" });
await textbox.fill(threadMessage);
await textbox.press("Enter");
await expect(threadPanel.locator(".mx_EventTile_last").getByText(threadMessage)).toBeVisible();

View File

@@ -461,11 +461,11 @@ test.describe("Timeline", () => {
// Send a emote
await page
.locator(".mx_RoomView_body")
.getByRole("textbox", { name: "Send a message…" })
.getByRole("textbox", { name: "Send an unencrypted message…" })
.fill("/me says hello to Mr. Bot");
await page
.locator(".mx_RoomView_body")
.getByRole("textbox", { name: "Send a message…" })
.getByRole("textbox", { name: "Send an unencrypted message…" })
.press("Enter");
// Check inline start margin of its avatar
// Here --right-padding is for the avatar on the message line

View File

@@ -107,6 +107,7 @@ interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
includeDialogBackground?: boolean;
showTooltips?: boolean;
timeout?: number;
hideJumpToBottomButton?: boolean;
}
type Expectations = {
@@ -165,6 +166,14 @@ export const expect = baseExpect.extend<Expectations>({
`;
}
if (options?.hideJumpToBottomButton) {
css += `
.mx_JumpToBottomButton {
display: none !important;
}
`;
}
if (options?.css) {
css += options.css;
}

View File

@@ -213,4 +213,26 @@ export class ElementAppPage {
.getByRole("button", { name: "Dismiss" })
.click();
}
/**
* Scroll an infinite list to the bottom.
* @param list The element to scroll
*/
public async scrollListToBottom(list: Locator): Promise<void> {
// First hover the mouse over the element that we want to scroll
await list.hover();
const needsScroll = async () => {
// From https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
const fullyScrolled = await list.evaluate(
(e) => Math.abs(e.scrollHeight - e.clientHeight - e.scrollTop) <= 1,
);
return !fullyScrolled;
};
// Scroll the element until we detect that it is fully scrolled
do {
await this.page.mouse.wheel(0, 1000);
} while (await needsScroll());
}
}

View File

@@ -0,0 +1,74 @@
/*
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.
*/
// Note: eslint-plugin-jsdoc doesn't like import types as parameters, so we
// get around it with @typedef
/**
* @typedef {import("@element-hq/element-web-module-api").Api} Api
*/
export default class CustomComponentModule {
static moduleApiVersion = "^1.2.0";
/**
* Basic module for testing.
* @param {Api} api API object
*/
constructor(api) {
this.api = api;
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Do not show edits",
(_props, originalComponent) => {
return originalComponent();
},
{ allowEditingEvent: false },
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Fall through here",
(props) => {
const body = props.mxEvent.content.body;
return `Fallthrough text for ${body}`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => {
if (evt.content.body === "Crash the filter!") {
throw new Error("Fail test!");
}
return false;
},
() => {
return `Should not render!`;
},
);
this.api.customComponents.registerMessageRenderer(
(evt) => evt.content.body === "Crash the renderer!",
() => {
throw new Error("Fail test!");
},
);
this.api.customComponents.registerMessageRenderer(
(mxEvent) => mxEvent.type === "m.room.message" && mxEvent.content.msgtype === "m.image",
(_props, originalComponent) => {
return originalComponent();
},
{ allowDownloadingMedia: async (mxEvent) => mxEvent.content.body !== "bad.png" },
);
// Order is specific here to avoid this overriding the other renderers
this.api.customComponents.registerMessageRenderer("m.room.message", (props, originalComponent) => {
const body = props.mxEvent.content.body;
if (body === "Do not replace me") {
return originalComponent();
} else if (body === "Fall through here") {
return null;
}
return `Custom text for ${body}`;
});
}
async load() {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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