Compare commits

...

165 Commits

Author SHA1 Message Date
Half-Shot
d7a185baea Move props into IProps 2025-03-17 10:07:36 +00:00
Half-Shot
93009d4613 Add a comment 2025-03-17 10:01:12 +00:00
Half-Shot
71257d97e7 Merge remote-tracking branch 'origin/develop' into hs/add-hide-image-button 2025-03-17 09:59:37 +00:00
Half-Shot
60eeb8a7de Support functional components for message body rendering. 2025-03-17 09:59:25 +00:00
Michael Telatynski
ff1da50dd9 Move a bunch of shared playwright code into @element-hq/element-web-playwright-common (#29477)
* Move a bunch of shared playwright code into @element-hq/element-web-playwright-common

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

* Remove stale devDep

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

* Iterate

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

* Update playwright-common

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

* Iterate

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

* Update screenshot

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

* Fix testcontainers version

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-17 09:16:45 +00:00
Half-Shot
571a2e373d Fixup tests 2025-03-17 09:06:58 +00:00
Will Hunt
4af5d4ac80 Do not lint playwright files. (#29510) 2025-03-17 09:05:21 +00:00
Half-Shot
d0b8564660 Fixup MImageBody test 2025-03-17 08:47:43 +00:00
Will Hunt
28ea91566a Merge branch 'develop' into hs/add-hide-image-button 2025-03-17 08:27:55 +00:00
Half-Shot
ef32747473 Drop setting hook usage. 2025-03-17 08:27:15 +00:00
Half-Shot
7696516e8b Use a hook for media visibility. 2025-03-17 08:22:19 +00:00
ElementRobot
a858fed321 [create-pull-request] automated change (#29509)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-17 06:22:14 +00:00
Florian Duros
f3dbe81ef4 New room list: add more options menu on room list item (#29445)
* refactor(room list item): rename `RoomListCell` into `RoomListItemView`

* refactor(room list item): move open room action to new room list item view model

* feat(hover menu): add `hasAccessToOptionsMenu`

* feat(hover menu): add to `RoomListItemViewModel` the condition to display or not the hover menu

* feat(hover menu): add view model for the hover menu

* feat(hover menu): add hover menu view

* feat(hover menu): add hover menu to room list item

* feat(hover menu): update i18n

* test(view model list item): update test and add test to `showHoverMenu`

* test(room list): update snapshot

* test(room list item menu): add tests for view model

* test(room list item menu): add tests for view

* test(room list item): add tests

* test(e2e): add tests for more options menu

* chore: update compound web

* test(e2e): fix typo
2025-03-14 16:22:45 +00:00
Florian Duros
ceba762caf New room list: fix compose menu action in space (#29500)
* fix(room list header): in view model, can create room in space if user has the right. Display the compose menu if the user can create room or video room

* fix(room list header): can create directly chat if compose menu is disabled

* test(room list header): add tests for view model

* test(room list header): update tests of view

* test(room list header): update list test
2025-03-14 16:12:30 +00:00
R Midhun Suresh
9fb52e984c RoomListViewModel: Provide a way to resort the room list and track the active sort method (#29499)
* Add a hook that deals with the sorting behaviour

Hook will provide a function to sort the list and also provides a state
which tracks the currently active sort option.

* Use hook in vm

* Write test for the vm

* Fix broken view tests
2025-03-14 15:10:34 +00:00
Florian Duros
c31f5521ec feat(room list): change *All rooms* meta space name to *All Chats* (#29498) 2025-03-14 12:47:40 +00:00
Will Hunt
66d9d717c4 Add setting to hide avatars of rooms you have been invited to. (#29497)
* Add ability to block images of rooms you have been invited to.

* strings

* Add tests

* fix snapshot

* tweaks

* lint
2025-03-14 12:03:09 +00:00
ElementRobot
4e3daa5df5 [create-pull-request] automated change (#29495)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-14 11:59:45 +00:00
David Baker
f9a0bb2904 Add analytics constant to encryption tab (#29489)
As it got stuck in review for 5 months and we all forgot about it
2025-03-14 10:06:44 +00:00
R Midhun Suresh
f4b03a1b06 Room List Store: Save preferred sorting algorithm and use that on app launch (#29493)
* Add `type` property to Sorter

So that we can uniquely identify any given sorting algorithm.

* Add a getter for the active sort algorithm

* Define a setting to store the sorting algorithm

* Add a method to resort the list of rooms

- Just one method where you specify the sorting algorithm by type.
- Persist the new sorting algorithm using SettingsStore.

* On startup, use preferred sorter

* Add tests
2025-03-14 09:41:04 +00:00
David Baker
be3778bef0 Add key storage toggle to Encryption settings (#29310)
* Add key storage toggle to Encryption settings

* Keys in the acceptable order

* Fix some tests

* Fix import

* Fix toast showing condition

* Fix import order

* Fix playwright tests

* Fix bits lost in merge

* Add key storage delete confirm screen

* Fix hardcoded Element string

* Fix type imports

* Fix tests

* Tests for key storage delete panel

* Fix test

* Type import

* Test for the view model

* Fix type import

* Actually fix type imports

* Test updating

* Add playwright test & clarify slightly confusing comment

* Show the advnced section whatever the state of key storage

* Update screenshots

* Copy css to its own file

* Add missing doc & merge loading states

* Add tsdoc & loading alt text to spinner

* Turn comments into proper tsdoc

* Switch to TypedEventEmitter and remove unnecessary loading state

* Add screenshot

* Use higher level interface

* Merge the two hooks in EncryptionUserSettingsTab

* Remove unused import

* Don't check key backup enabled state separately

as we don't need it for all the screens

* Update snapshot

* Use fixed recovery key function

* Amalgamate duplicated CSS files

* Have "key storage disabled" as a separate state

* Update snapshot

* Fix... bad merge?

* Add backup enabled mock to more tests

* More snapshots

* Use defer util

* Update to use EncryptionCardButtons

* Update snapshots

* Use EncryptionCardEmphasisedContent

* Update snapshots

* Update snapshot

* Try screenshot from CI playwright

* Try playwright screenshots again

* More screenshots

* Rename to match files

* Test that 4S secrets are deleted

* Make description clearer

* Fix typo & move related states together

* Add comment

* More comments

* Fix hook docs

* restoreAllMocks

* Update snapshot

because pulling in upstream has caused IDs to shift

* Switch icon

as apparenty the error icon has changed

* Update snapshot

* Missing copyright

* Re-order states

and also sort out indenting

* Remove phantom space

* Clarify 'button'

* Clarify docs more

* Explain thinking behind updating

* Switch to getActiveBackupVersion

which checks that key backup is happining on this device, which is
consistent with EX.

* Add use of Key Storage Panel

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

* Change key storage panel to be consistent

ie. using getActiveBackupVersion(), and add comment

* Add tsdoc

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

* Use BACKUP_DISABLED_ACCOUNT_DATA_KEY in more places

* Expand doc

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

* Undo random yarn lock change

* Use aggregate method for disabling key storage

in https://github.com/matrix-org/matrix-js-sdk/pull/4742

* Fix tests

* Use key backup status event to update

* Comment formatting

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

* Fix comment & put check inside if statement

* Add comment

* Prettier

* Fix comment

* Update snapshot

Which has gained nowrap due to 917d53a56f

---------

Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-03-14 08:52:41 +00:00
ElementRobot
973d639d01 [create-pull-request] automated change (#29494)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-14 06:15:40 +00:00
Florian Duros
20d8abf7c2 New room list: add primary filters (#29481)
* feat(room filter): add component for the primary filters

* feat(room filter): add filter component to room list view

* test(room filter): add tests to primary filters

* test: update snapshots

* test(e2e): update snapshots

* test(e2e): add tests for primary filters

* refactor: change aria-label of primary filters
2025-03-13 17:29:57 +00:00
Will Hunt
46b1234a1d Merge branch 'develop' into hs/add-hide-image-button 2025-03-13 16:49:27 +00:00
Tulir Asokan
fda658182a Implement MSC4142: Remove unintentional intentional mentions in replies (#28209)
* Implement MSC4142: Remove unintentional intentional mentions in replies

* Fix comment
2025-03-13 16:00:54 +00:00
renovate[bot]
9bfea92b66 Update testcontainers-node monorepo to v10.19.0 (#29491)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-13 15:46:27 +00:00
Michael Telatynski
962136d453 Avoid using /tmp/ for bind mounts and non-tmpfs binds (#29488)
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-13 14:51:01 +00:00
Florian Duros
917d53a56f Add wrap props to flex component (#29480)
* feat(flex): add wrap props to flex component

* test: update snapshot
2025-03-13 13:32:48 +00:00
Máté Gyöngyösi
e44ca88a7e Change ToggleHiddenEventVisibility & GoToHome KeyBindingActions (#29374)
The current keyboard shortcuts for GoToHome and ToggleHiddenEventVisibility are:

|                             	| other        	| macOS        	|
|-----------------------------	|--------------	|--------------	|
| GoToHome                    	| Ctrl–Alt–H   	| Ctrl–Shift–H 	|
| ToggleHiddenEventVisibility 	| Ctrl–Shift–H 	| Cmd–Shift–H  	|

This removes both distinctions for macOS in order ToggleHiddenEventVisibility not to conflict with...
1. the built-in Safari keyboard shortcut for opening the Home page (Cmd–Shift–H)
2. the KeyBindingAction for GoToHome.

Co-authored-by: Florian Duros <florianduros@element.io>
2025-03-13 09:46:29 +00:00
renovate[bot]
cb7d77de45 Update dependency @babel/runtime to v7.26.10 [SECURITY] (#29478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-12 12:45:21 +00:00
renovate[bot]
cd6737942f Update dependency @babel/runtime to v7.26.10 [SECURITY] (#29478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-12 12:45:21 +00:00
renovate[bot]
a058d85c21 Update playwright to v1.51.0 (#29469)
* Update playwright to v1.51.0

* Update screenshots

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-03-12 11:50:10 +00:00
Andy Balaam
bf6ae73d39 White background for 'They do not match' button (#29470) 2025-03-12 08:04:17 +00:00
ElementRobot
273cdf41e9 [create-pull-request] automated change (#29476)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-12 06:20:02 +00:00
ElementRobot
3ab3041c45 [create-pull-request] automated change (#29475)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-12 06:14:50 +00:00
Ben Banfield-Zanin
2052080d7d Fix Docker Healthcheck (#29471)
* Correct test for docker container being healthy

* Correct docker healthcheck for busybox wget

* Repeatedly check the health state of the docker container

* Use until loop & rely on timeout-minutes

* Fix check to look at healthy state

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-11 18:04:33 +00:00
Half-Shot
b9c0d63e3e lint 2025-03-11 15:57:09 +00:00
Half-Shot
cf7e52c6fc lint 2025-03-11 15:55:30 +00:00
Half-Shot
e87eb127ee Add tests for HideActionButton 2025-03-11 15:51:05 +00:00
Half-Shot
83e421daf2 appese prettier 2025-03-11 15:20:56 +00:00
Half-Shot
d6fb24dea7 i18n 2025-03-11 15:14:46 +00:00
Half-Shot
a518c8d662 add type 2025-03-11 15:14:25 +00:00
Half-Shot
c759e516bd docs fixes 2025-03-11 15:13:42 +00:00
Half-Shot
c8b55c3dfe add description for migration 2025-03-11 15:11:14 +00:00
Half-Shot
7197093744 Fixup and add tests 2025-03-11 15:08:48 +00:00
dependabot[bot]
cc95d154fb Bump axios from 1.8.1 to 1.8.2 (#29468)
Bumps [axios](https://github.com/axios/axios) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 15:03:42 +00:00
RiotRobot
4e696d2dc6 Reset matrix-js-sdk back to develop branch 2025-03-11 14:44:08 +00:00
RiotRobot
610b14adc1 Merge branch 'master' into develop 2025-03-11 14:43:56 +00:00
RiotRobot
324dd5a858 v1.11.95 2025-03-11 14:40:55 +00:00
RiotRobot
bc4bc6c25e Upgrade dependency to matrix-js-sdk@37.1.0 2025-03-11 14:36:16 +00:00
R Midhun Suresh
3f3fba99eb RoomListViewModel: Support secondary filters in the view model (#29465)
* Support secondary filters in the view model

* Write view model tests

* Fix RoomList test

* Add more comments
2025-03-11 13:29:55 +00:00
Half-Shot
4e34adb854 Tweaks to MImageBody to support new setting. 2025-03-11 11:32:49 +00:00
Half-Shot
72c2a3eb07 Add an action button to hide settings. 2025-03-11 11:32:26 +00:00
Half-Shot
4d290461c4 Add a migration path 2025-03-11 11:32:17 +00:00
Half-Shot
0cc06450d7 Add new setting showMediaEventIds 2025-03-11 11:32:10 +00:00
Half-Shot
9376d71831 Move useSettingsValueWithSetter to useSettings 2025-03-11 11:31:51 +00:00
ElementRobot
26a17f9314 [create-pull-request] automated change (#29455)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-11 10:34:05 +00:00
Half-Shot
6d5442a87b start hide 2025-03-11 10:13:06 +00:00
R Midhun Suresh
fd91e78152 RoomListViewModel: Support primary filters in the view model (#29454)
* Track available filters and expose this info from the vm

- Adds a separate hook that tracks the filtered rooms and the available
  filters.
- When secondary filters are added, some of the primary filters will be
  selectively hidden. So track this info in the vm.

* Write tests

* Fix typescript error

* Fix translation

* Explain what a primary filter is
2025-03-10 13:23:38 +00:00
R Midhun Suresh
af476905b6 Room List Store: Fetch rooms after space store is ready + attach store to window (#29453)
* Attach the new store to window

* Fetch rooms after space store is ready

If we fetch a list of rooms and then wait for the space store to be
ready, we will need some way of handling the onAction calls we get
while we wait. These calls are dropped now.
2025-03-10 12:33:06 +00:00
R Midhun Suresh
da87bbe854 Room List Store: Fix bug where left rooms appear in room list (#29452)
* Write failing test

* Remove room when membership changes from JOIN to LEAVE
2025-03-10 11:58:12 +00:00
R Midhun Suresh
47976447b5 Room List Store: Implement secondary filters (#29458)
* Implement the secondary filters

* Use the new filters in the store

* Write tests
2025-03-10 11:37:37 +00:00
Will Hunt
53065f9437 Add E2E test for quick settings dialog (#29441)
* Update quick settings menu to use a11y roles.

* Add e2e test to test quick menu rendering

* Use a testid for now.

* lint lint

* Revert aria changes

* revert managed

* write screenshot
2025-03-10 09:12:38 +00:00
ElementRobot
179b368809 [create-pull-request] automated change (#29447)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-08 06:14:47 +00:00
R Midhun Suresh
82957507d0 Room List Store: Implement rest of the primary filters (#29444)
* Implement rest of the primary filters

* Support the new filters in the store
2025-03-07 10:59:39 +00:00
ElementRobot
27c1b38dab [create-pull-request] automated change (#29443)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-07 06:20:44 +00:00
R Midhun Suresh
7ff1fd259d Room List Store: Support filters by implementing just the favourite filter (#29433)
* Implement the favourite filter

* Make the room node capable of dealing with filters

- Holds data to indicate which filters apply
- Provides method to check if a given set of filters apply to this node
- Provides a method to recalculate which filters apply

* Wire up the filtering mechanism in skip list

* Use filters in the store

* Remove else

* Use a set instead of map
2025-03-06 12:43:25 +00:00
Will Hunt
8d891cde53 Move toggle switch for integration manager for a11y (#29436)
* Move toggle switch for integration manager for a11y

* Update test

* add toggle_label

* lint
2025-03-06 11:55:41 +00:00
Florian Duros
90cc44b340 New room list: basic flat list (#29368)
* chore: make the room list panel a flexbox

* feat(new room list): add `RoomListCell` component

* feat(new room list): add virtualized `RoomList` component

* feat(new room list): add `RoomListView` component

* feat(new room list): use `RoomListView` in `RoomListPanel`

* test(new room list): add test for room cell

* test(new room list): update room list panel tests

* test(new room list): add test to virtualized room list

* test(e2e): add room list tests

* test(e2e): update room panel tests
2025-03-06 10:01:55 +00:00
David Langley
b6c872142b Add space to the bottom of the room summary actions below leave room (#29270)
* Add space to the bottom of the room summary actions below leave room

* 8x not 6x

* Spacing needs to be within the scoll content, add it to the bottom of the leave action

* Update RoomSummaryCard-test.tsx.snap

* Fix snapshot and add screenshot test
2025-03-06 08:44:29 +00:00
Will Hunt
3762d40620 Improve rageshake upload experience by providing useful error information (#29378)
* Refactor submit rageshake so that it uses the new error codes.

* Improve error information given in Bug Report Dialog

* use type

* Refactor with generic error & policy link.

* lint

* lint

* Add BugReportDialog test

* fix time travel

* use waitFor while waiting for fetch to finish

* lint

* Drop error prefix as per 3973bb38ef

* small fixes

* Don't change string here.

* Fixup i18n strings.
2025-03-06 08:38:41 +00:00
ElementRobot
42192cbe06 [create-pull-request] automated change (#29431)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-06 06:15:39 +00:00
Robin
aa996010b4 Show error screens in group calls (#29254)
* Avoid destroying calls until they are hidden from the UI

We often want calls to exist even when no more participants are left in the MatrixRTC session. So, we should avoid destroying calls as long as they're being presented in the UI; this means that the user has an intent to either join the call or continue looking at an error screen, and we shouldn't interrupt that interaction.

The RoomViewStore is now what takes care of creating and destroying calls, rather than the CallView. In general it seems kinda impossible to safely create and destroy model objects from React lifecycle hooks, so moving this responsibility to a store seemed appropriate and resolves existing issues with calls in React strict mode.

* Wait for a close action before closing a call

This creates a distinction between the user hanging up and the widget being ready to close, which is useful for allowing Element Call to show error screens when disconnected from the call, for example.

* Don't expect a 'close' action in video rooms

These use the returnToLobby option and are expected to remain visible when the user leaves the call.
2025-03-05 20:41:26 +00:00
R Midhun Suresh
e9a3625bd6 Add more functionality to the room list vm (#29402)
* Add more vm functionality

- Listen for updates from the store
- Provide a method to open rooms

* Write test
2025-03-05 08:54:04 +00:00
ElementRobot
343dd06929 [create-pull-request] automated change (#29428)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-05 06:20:01 +00:00
ElementRobot
77b6c3b526 [create-pull-request] automated change (#29427)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-05 06:15:34 +00:00
renovate[bot]
b721505211 Update all non-major dependencies (#29415)
* Update all non-major dependencies

* 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-03-04 17:03:32 +00:00
Hubert Chathi
56c7fc1948 Prevent user from accidentally triggering multiple identity resets (#29388)
* prevent user from accidentally triggering multiple identity resets

* apply changes from review and update to latest design

* Use a CSS class and compound variable

* update snapshot

* Update test/unit-tests/components/views/settings/encryption/ResetIdentityPanel-test.tsx

---------

Co-authored-by: Richard van der Hoff <richard@matrix.org>
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
2025-03-04 16:40:35 +00:00
renovate[bot]
9d8efacede Update dependency @vector-im/matrix-wysiwyg to v2.38.2 (#28708)
* Update dependency @vector-im/matrix-wysiwyg to v2.38.2

* Fix 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-03-04 15:36:57 +00:00
renovate[bot]
1770b94ed3 Update dependency typescript to v5.8.2 (#29417)
* Update dependency typescript to v5.8.2

* Fix 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-03-04 15:01:20 +00:00
renovate[bot]
dfdac8ef63 Update dependency copy-webpack-plugin to v13 (#29423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:51:37 +00:00
renovate[bot]
f1ebd85af1 Update dependency babel-loader to v10 (#29422)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:48:57 +00:00
renovate[bot]
4776a9971d Update typescript-eslint monorepo to v8.25.0 (#29420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:48:06 +00:00
renovate[bot]
20ac69f379 Update guibranco/github-status-action-v2 digest to 5ef6e17 (#29409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:48:00 +00:00
renovate[bot]
8c42b0bed8 Update stylelint (#29419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:46:16 +00:00
renovate[bot]
fbc6f12408 Update dependency @types/react-virtualized to v9.22.2 (#29413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:46:13 +00:00
renovate[bot]
b82c8554e3 Update fontsource monorepo to v5.2.5 (#29418)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:46:12 +00:00
renovate[bot]
3d705b1895 Update dependency caniuse-lite to v1.0.30001701 (#29414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:45:47 +00:00
renovate[bot]
81c12db5ee Update dependency @sentry/browser to v9.3.0 (#29416)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:45:44 +00:00
renovate[bot]
e1d76e77a5 Update definitelyTyped (#29412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:45:39 +00:00
renovate[bot]
54e015706c Update peter-evans/create-pull-request digest to 271a8d0 (#29410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:45:16 +00:00
renovate[bot]
cef25c2cab Update sigstore/cosign-installer digest to d7d6bc7 (#29411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:44:45 +00:00
renovate[bot]
59c26fc3ad Update docker (#29408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 14:43:55 +00:00
Michael Telatynski
31af8b07dd Remove buggy tooltip on room intro & homepage (#29406)
* Remove buggy tooltip on room intro & homepage

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

* Add knip ignore

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-04 13:24:38 +00:00
RiotRobot
c0d14daa17 v1.11.95-rc.0 2025-03-04 12:51:06 +00:00
RiotRobot
ed35a7cba4 Upgrade dependency to matrix-js-sdk@37.1.0-rc.0 2025-03-04 12:46:15 +00:00
R Midhun Suresh
21e9d93e69 Room List Store: Filter rooms by active space (#29399)
* Add method to await space store setup

Otherwise, the room list store will get incorrect information about
spaces and thus will produce an incorrect roomlist.

* Implement a way to filter by active space

Implement a way to filter by active space

* Fix broken jest tests

* Fix typo

* Rename `isReady` to `storeReadyPromise`

* Fix mock in test
2025-03-04 11:27:41 +00:00
Richard van der Hoff
ffa8971195 Device dehydration: remove .well-known check (#29404)
* Device dehydrateion: remove .well-known check

Per https://github.com/element-hq/element-web/issues/29387, this is redundant

* Update src/utils/device/dehydration.ts

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-03-04 10:41:16 +00:00
ElementRobot
072ee0cf36 [create-pull-request] automated change (#29405)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-04 09:26:49 +00:00
Richard van der Hoff
4b02520453 Remove redundant UserIdentityWarning test (#29403)
* Remove redundant `UserIdentityWarning` test

Since the refactoring in https://github.com/element-hq/element-web/pull/29067,
this test is redundant.

It is also flaky and hard to understand. Time for it to die.

* delint
2025-03-03 15:28:46 +00:00
R Midhun Suresh
bf48100d31 Room List - Update the room list store on actions from the dispatcher (#29397)
* Update the store on action

* Add more tests

* Add newlines between case blocks

* Make code more readable

- Make if/else more consistent
- Add comment on findAndAddRoom()

* Add more tests

* Remove redundant code

On a timeline action, we return early if payload.room is falsy.
So then why do we need to retry fetching the room?
I think this can be removed but will ask others if there's some
conext I'm missing.

* Fix test

* Remove more redundant code

* Add more tests

* Explain intention in comment

* Emit only once even when adding multiple rooms

* Add missing tsdoc
2025-03-03 15:08:14 +00:00
R Midhun Suresh
2da21248bb Room List - Implement a minimal view model (#29357)
* Implement enough of the new store to get a list of rooms

* Make it possible to swap sorting algorithm

* Don't attach to window object

We don't want the store to be created if the labs flag is off

* Remove the store class

Probably best to include this PR with the minimal vm implmentation

* Create a new room list store that wraps around the skip list

* Create a minimal view model

* Fix CI

* Add some basic tests for the store

* Write more tests

* Add some jsdoc comments

* Add more documentation

* Add more docs
2025-03-03 11:12:00 +00:00
Florian Duros
3c57323595 Fix edited code block width (#29394)
* fix (event tile): make the markdown body take all the width when edited

* test (e2e): add code block test
2025-03-03 09:17:41 +00:00
ElementRobot
4c72f0c0b2 [create-pull-request] automated change (#29400)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-03-03 06:20:30 +00:00
ElementRobot
dfd08a8c01 Playwright Docker image updates (#29355)
* [create-pull-request] automated change

* Update synapse.ts

* Update synapse.ts

---------

Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-28 11:32:53 +00:00
Florian Duros
7db909a47d new room list: keep space name in one line in header (#29369)
* fix(new room list): keep space name in one line in header

* test(new room list): update tests
2025-02-28 08:34:06 +00:00
ElementRobot
1ad1387e05 [create-pull-request] automated change (#29389)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-28 06:21:55 +00:00
Michael Telatynski
c6b3bf962a Enable babel plugins to enable js-sdk to use decorators (#29376)
* Enable @babel/plugin-proposal-decorators

Only needed because we consume js-sdk code directly so its own transpiling isn't in play

This should separately be fixed, we should not need to have a superset of js-sdk's babel plugins

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-02-27 14:20:01 +00:00
Florian Duros
e749b017c9 refactor: rename mx_RoomList as mx_LegacyRoomList (#29362) 2025-02-27 13:34:54 +00:00
RiotRobot
c00262f0c5 Merge branch 'master' into develop 2025-02-27 13:19:26 +00:00
RiotRobot
7a513a2dc2 v1.11.94 2025-02-27 13:16:17 +00:00
ElementRobot
808412c6be fix: /tmp/element-web-config may already exist preventing the container from booting up (#29372) (#29377)
* fix: /tmp/element-web-config may already exist preventing the container from booting up

* Update docker/docker-entrypoint.d/18-load-element-modules.sh

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
(cherry picked from commit 45497905be)

Co-authored-by: Itay Grudev <itay+github.com@grudev.com>
2025-02-27 12:41:59 +00:00
Itay Grudev
45497905be fix: /tmp/element-web-config may already exist preventing the container from booting up (#29372)
* fix: /tmp/element-web-config may already exist preventing the container from booting up

* Update docker/docker-entrypoint.d/18-load-element-modules.sh

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-27 12:13:23 +00:00
Florian Duros
0997e0a747 refactor: rename RoomListView as RoomListPanel (#29361) 2025-02-26 11:14:04 +00:00
ElementRobot
6173c1224b [create-pull-request] automated change (#29364)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-26 06:23:19 +00:00
Florian Duros
f95218e2b7 New room list: add space menu in room header (#29352)
* feat(new room list): add space menu in view model

* test(new room list): add space menu in view model

* feat(new room list): add space menu in room list header

* chore: update i18n

* test(new room list): add tests for space menu

* test(new room list): update room list tests

* test(e2e): add tests for space menu
2025-02-25 14:06:01 +00:00
Hubert Chathi
62a287219d add test to ensure that dehydrated device gets removed on identity reset (#29354) 2025-02-25 13:46:57 +00:00
RiotRobot
db45a17d43 Reset matrix-js-sdk back to develop branch 2025-02-25 13:46:58 +00:00
RiotRobot
9b1de5634d Merge branch 'master' into develop 2025-02-25 13:46:45 +00:00
RiotRobot
230e26e1ab v1.11.93 2025-02-25 13:27:04 +00:00
Michael Telatynski
cf5ffacff2 Upgrade dependency to matrix-js-sdk@37.0.0 2025-02-25 13:17:51 +00:00
R Midhun Suresh
fe353542cb Room List - Store sorted rooms in skip list (#29345)
* Implement a skip list for storing rooms

This data structure stores rooms in a given sorted order and allows for
very fast insertions and deletions.

* Export function to get last timestamp of room

* Write tests for the skip list

* Implement enough of the new store to get a list of rooms

* Make it possible to swap sorting algorithm

* Fix comment

* Don't attach to window object

We don't want the store to be created if the labs flag is off

* Remove the store class

Probably best to include this PR with the minimal vm implmentation
2025-02-25 13:12:37 +00:00
Michael Telatynski
0b624bf645 Update release.yml permissions 2025-02-25 13:20:03 +00:00
Richard van der Hoff
3cc1ccd029 Dismiss "Key storage out of sync" toast when secrets received (#29348)
* DeviceListener: improve logging

use a LogSpan to tie together logs from the same run, and add some more logs
for various cases

* Regression playwright test

* Remove unused mocking of `getCrossSigningId`

DeviceListener no longer reads this thing

* Clean up unit tests

Remove redundant describe block

* Remove the "out of sync" toast when we are no longer out of sync

Receiving the crypto secrets via secret sharing should make the toast go away.
2025-02-25 12:13:42 +00:00
Michael Telatynski
05b8fff58a Dynamically load Element Web modules in Docker entrypoint (#29346) (#29358)
* Dynamically load Element Web modules in Docker entrypoint



* Iterate



* Drop environment for PR runs



* Iterate



---------



(cherry picked from commit 8ef84349b5)

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-25 11:57:57 +00:00
Florian Duros
43efd911c7 New room list: add dial to search section (#29359)
* feat(new room list): add dial to new room list

* test(new room list): test dial button

* test(new room list): update room list test
2025-02-25 11:03:29 +00:00
Michael Telatynski
2e883b40eb Add docker healthcheck (#29351)
* Add docker healthcheck

and test ELEMENT_WEB_PORT envvar

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>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-25 09:16:22 +00:00
Florian Duros
14e3a77dc2 Minor CSS fixes for the new room list (#29334)
* refactor: use `var(--cpd-border-width-1)` instead of `1px`

* refactor: use header font in header
2025-02-24 18:59:26 +00:00
Florian Duros
efc6149a8b Update @vector-im/compound-design-tokens & @vector-im/compound-web (#29307)
* chore: update `@vector-im/compound-design-tokens` & `@vector-im/compound-web` to last version

* chore: use `error-solid` icon instead of `error`

* chore: update jest snapshot

* fix: `AccessibleButton` lint
2025-02-24 17:08:12 +00:00
Michael Telatynski
8ef84349b5 Dynamically load Element Web modules in Docker entrypoint (#29346)
* Dynamically load Element Web modules in Docker entrypoint

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

* Iterate

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

* Drop environment for PR runs

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-02-24 16:54:57 +00:00
Florian Duros
7d94fa9b03 New room list: add compose menu for spaces in header (#29347)
* feat(new room list): add compose menu in header for spaces

* test(new room list): add tests for space

* test(e2e new room list): update space test

* chore: formatting and reuse type var
2025-02-24 15:51:50 +00:00
ElementRobot
37136ecf46 [create-pull-request] automated change (#29342)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-24 06:21:38 +00:00
ElementRobot
99b9eee86e [create-pull-request] automated change (#29338)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-22 06:14:13 +00:00
David Langley
27c0e97e44 Fix test that doesn't make sense in usePublicRoomDirectory-test.tsx (#29335)
* remove test that doesn't make sense

* Actually let's fix the test rather than remove it

* Add comment, remove consoles.
2025-02-22 00:17:13 +00:00
David Langley
0cbc6f99d0 Set language explicitly (#29332) 2025-02-22 00:16:33 +00:00
David Langley
d8904a6e56 getByRole is slow on large trees, use getByText (#29331) 2025-02-21 13:45:52 +00:00
Will Hunt
22943ee06a Use EditInPlace control for Identity Server picker to improve a11y (#29280)
* Use EditInPlace for identity server picker.

* Exclude picker from default dialog button styles.

* Remove unused import.

* Update test

* Remove unused css

* Update test

* drop only

* Add a test for setting an ID server.

* Add a unit test for SetIdServer

* fix tests

* Reformat mx_Dialog button :not list to use a more readable selector.

* Reformat other :not sections

* forgot a comma

* We're in 2025 now.

* Update copyright + use class methods.
2025-02-21 13:18:14 +00:00
David Langley
2e1798edc4 Add padding to room header icon (#29271)
* Add padding to room header icon

* Add screenshot tests for room header icon

* Add type to visibility import

* Use visibility type

* lint
2025-02-21 11:27:57 +00:00
ElementRobot
3470182410 [create-pull-request] automated change (#29327)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-21 06:19:03 +00:00
ElementRobot
9d8d38eeb8 [create-pull-request] automated change (#29326)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-21 06:15:12 +00:00
Florian Duros
fb57924350 First step to add header to new room list (#29320)
* feat: create new header

* test: add tests to view model

* test: add tests to view

* feat: add header to new room list

* test(e2e): update RoomListView snapshot

* test(e2e): add tests for room list header

* refactor: minor code improvement
2025-02-20 15:40:19 +00:00
David Baker
e8954f08ce Move CSS for ResetIdentityPanel into EncryptionCard (#29311)
* Move CSS for ResetIdentityPanel into EncryptionCard

This allows it to be re-used in other components. It's a *bit* magic
that EncryptionCard applies style to divs within it, although it
somewhat makes sense that it wants them styled a particular way.
The alternative would be to add another component for a div child of
encryption card like EncryptionCardButtons that just makes it flexbox
and centered: I'm not sure which is better.

* Update snapshot

* Update snapshot

* Do it the other way

Because we only want it in the destructive cards, not the other ones.

* Use flex component

* Also use gap prop and update snapshots

* Fix justification

* Snaspshots again

* Set align-items to normal

As center affected the list items too. Also add it to the flex
component because it didn't have it as an option.
2025-02-20 13:20:49 +00:00
ElementRobot
e161f9fb18 [create-pull-request] automated change (#29321)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-20 06:14:23 +00:00
Michael Telatynski
e47d7aaaff Add Windows 64-bit arm link and remove 32-bit link on compatibility page (#29312)
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page

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

* Update snapshots

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

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 16:17:56 +00:00
Florian Duros
8857c07acb Move view_create_chat & view_create_room to actions.ts (#29319)
* refactor: replace `view_create_chat` by `Action.CreateChat`

* refactor: replace `view_create_room` by `Action.CreateRoom`
2025-02-19 15:11:43 +00:00
Florian Duros
28ed506fe1 refactor: rename RoomListHeader as LegacyRoomListHeader (#29308) 2025-02-19 14:06:01 +00:00
R Midhun Suresh
76b3be6263 Add some basic documentation for MVVM (#29316) 2025-02-19 13:13:28 +00:00
ElementRobot
6c768b8b32 [create-pull-request] automated change (#29313)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-19 10:39:36 +00:00
ElementRobot
809ada17a4 [create-pull-request] automated change (#29314)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-19 06:19:54 +00:00
renovate[bot]
c7762a80f1 Update dependency @sentry/browser to v9 (#29303)
* Update dependency @sentry/browser to v9

* Remove redundant option

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-02-18 21:11:36 +00:00
renovate[bot]
261923832d Update dependency caniuse-lite to v1.0.30001699 (#29297)
* Update dependency caniuse-lite to v1.0.30001699

* Update tests

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-02-18 20:29:47 +00:00
renovate[bot]
3daa1bf06a Update all non-major dependencies (#29298)
* 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-02-18 20:20:06 +00:00
renovate[bot]
e5c8d7dbf0 Update playwright to v1.50.1 (#29183)
* Update playwright to v1.50.1

* Update snapshots

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-02-18 18:43:40 +00:00
ElementRobot
441119ca3a [create-pull-request] automated change (#29286)
Co-authored-by: t3chguy <2403652+t3chguy@users.noreply.github.com>
2025-02-18 17:49:15 +00:00
renovate[bot]
acb3e781a4 Update typescript-eslint monorepo to v8.24.0 (#29302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:59:10 +00:00
renovate[bot]
3fb1f6ef4d Update testcontainers-node monorepo to v10.18.0 (#29301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:55:48 +00:00
renovate[bot]
cbfbfad959 Update docker (#29291)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:27:32 +00:00
renovate[bot]
7546bbc1f0 Update dependency @sentry/browser to v8.55.0 (#29299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:10 +00:00
renovate[bot]
6a10c86d7a Update dependency @stylistic/eslint-plugin to v3.1.0 (#29300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:07:06 +00:00
renovate[bot]
6f9e3bfe3e Update dependency @types/node to v18.19.76 (#29296)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:06:06 +00:00
David Baker
61d55462df Honour the backup disable flag from Element X (#29290)
* Honour the backup disable flag from Element X

This unfortunately named and unspecced flag is set by Element X
to denote that the user has chosen to disable key storage and it
should not automatically try to enable it again. This changes Element
web to not prompt to enable recovery if this flag is set.

* Remove unnecessary conditional

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

---------

Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
2025-02-18 15:06:00 +00:00
renovate[bot]
d391c69e53 Update dependency @formatjs/intl-segmenter to v11.7.9 (#29295)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:23 +00:00
renovate[bot]
a6afff9759 Update babel monorepo (#29294)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:02:22 +00:00
renovate[bot]
073d8e0b86 Update sigstore/cosign-installer digest to c56c2d3 (#29293)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:53:35 +00:00
renovate[bot]
ecf5d720b0 Update guibranco/github-status-action-v2 digest to 7ca807c (#29292)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 14:52:13 +00:00
454 changed files with 13365 additions and 6006 deletions

View File

@@ -8,3 +8,6 @@ src/component-index.js
# Auto-generated file
src/modules.ts
src/modules.js
# Test result files
/playwright/test-results/
/playwright/html-report/

View File

@@ -3,6 +3,7 @@ on:
workflow_dispatch: {}
push:
tags: [v*]
pull_request: {}
schedule:
# This job can take a while, and we have usage limits, so just publish develop only twice a day
- cron: "0 7/12 * * *"
@@ -12,42 +13,91 @@ jobs:
buildx:
name: Docker Buildx
runs-on: ubuntu-24.04
environment: dockerhub
environment: ${{ github.event_name != 'pull_request' && 'dockerhub' || '' }}
permissions:
id-token: write # needed for signing the images with GitHub OIDC Token
packages: write # needed for publishing packages to GHCR
env:
TEST_TAG: vectorim/element-web:test
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for docker-package to be able to calculate the version
- name: Install Cosign
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
if: github.event_name != 'pull_request'
- name: Set up QEMU
uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # 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@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and load
id: test-build
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
with:
context: .
load: true
- name: Test the image
env:
IMAGEID: ${{ steps.test-build.outputs.imageid }}
timeout-minutes: 2
run: |
set -x
# Make a fake module to test the image
MODULE_PATH="modules/module_name/index.js"
mkdir -p $(dirname $MODULE_PATH)
echo 'alert("Testing");' > $MODULE_PATH
# Spin up a container of the image
ELEMENT_WEB_PORT=8181
CONTAINER_ID=$(
docker run \
--rm \
-e "ELEMENT_WEB_PORT=$ELEMENT_WEB_PORT" \
-dp "$ELEMENT_WEB_PORT:$ELEMENT_WEB_PORT" \
-v $(pwd)/modules:/modules \
"$IMAGEID" \
)
# Run some smoke tests
wget --retry-connrefused --tries=5 -q --wait=3 --spider "http://localhost:$ELEMENT_WEB_PORT/modules/module_name/index.js"
MODULE_0=$(curl "http://localhost:$ELEMENT_WEB_PORT/config.json" | jq -r .modules[0])
test "$MODULE_0" = "/${MODULE_PATH}"
# Check healthcheck
until test "$(docker inspect -f {{.State.Health.Status}} $CONTAINER_ID)" == "healthy"; do
sleep 1
done
# Clean up
docker stop "$CONTAINER_ID"
- name: Docker meta
id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
if: github.event_name != 'pull_request'
with:
images: |
vectorim/element-web
@@ -60,7 +110,8 @@ jobs:
- name: Build and push
id: build-and-push
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
if: github.event_name != 'pull_request'
with:
context: .
push: true
@@ -72,6 +123,7 @@ jobs:
env:
DIGEST: ${{ steps.build-and-push.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
if: github.event_name != 'pull_request'
run: |
images=""
for tag in ${TAGS}; do
@@ -81,6 +133,7 @@ jobs:
- name: Update repo description
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
if: github.event_name != 'pull_request'
continue-on-error: true
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/playwright-image-updates

View File

@@ -19,6 +19,7 @@ jobs:
contents: write
issues: write
pull-requests: read
id-token: write
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}

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

View File

@@ -23,7 +23,7 @@ jobs:
run: "yarn update:jitsi"
- name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
with:
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
branch: actions/jitsi-update

View File

@@ -1,3 +1,65 @@
Changes in [1.11.95](https://github.com/element-hq/element-web/releases/tag/v1.11.95) (2025-03-11)
==================================================================================================
## ✨ Features
* Room List Store: Filter rooms by active space ([#29399](https://github.com/element-hq/element-web/pull/29399)). Contributed by @MidhunSureshR.
* Room List - Update the room list store on actions from the dispatcher ([#29397](https://github.com/element-hq/element-web/pull/29397)). Contributed by @MidhunSureshR.
* Room List - Implement a minimal view model ([#29357](https://github.com/element-hq/element-web/pull/29357)). Contributed by @MidhunSureshR.
* New room list: add space menu in room header ([#29352](https://github.com/element-hq/element-web/pull/29352)). Contributed by @florianduros.
* Room List - Store sorted rooms in skip list ([#29345](https://github.com/element-hq/element-web/pull/29345)). Contributed by @MidhunSureshR.
* New room list: add dial to search section ([#29359](https://github.com/element-hq/element-web/pull/29359)). Contributed by @florianduros.
* New room list: add compose menu for spaces in header ([#29347](https://github.com/element-hq/element-web/pull/29347)). Contributed by @florianduros.
* Use EditInPlace control for Identity Server picker to improve a11y ([#29280](https://github.com/element-hq/element-web/pull/29280)). Contributed by @Half-Shot.
* First step to add header to new room list ([#29320](https://github.com/element-hq/element-web/pull/29320)). Contributed by @florianduros.
* Add Windows 64-bit arm link and remove 32-bit link on compatibility page ([#29312](https://github.com/element-hq/element-web/pull/29312)). Contributed by @t3chguy.
* Honour the backup disable flag from Element X ([#29290](https://github.com/element-hq/element-web/pull/29290)). Contributed by @dbkr.
## 🐛 Bug Fixes
* Fix edited code block width ([#29394](https://github.com/element-hq/element-web/pull/29394)). Contributed by @florianduros.
* new room list: keep space name in one line in header ([#29369](https://github.com/element-hq/element-web/pull/29369)). Contributed by @florianduros.
* Dismiss "Key storage out of sync" toast when secrets received ([#29348](https://github.com/element-hq/element-web/pull/29348)). Contributed by @richvdh.
* Minor CSS fixes for the new room list ([#29334](https://github.com/element-hq/element-web/pull/29334)). Contributed by @florianduros.
* Add padding to room header icon ([#29271](https://github.com/element-hq/element-web/pull/29271)). Contributed by @langleyd.
Changes in [1.11.94](https://github.com/element-hq/element-web/releases/tag/v1.11.94) (2025-02-27)
==================================================================================================
## 🐛 Bug Fixes
* [Backport staging] fix: /tmp/element-web-config may already exist preventing the container from booting up ([#29377](https://github.com/element-hq/element-web/pull/29377)). Contributed by @RiotRobot.
Changes in [1.11.93](https://github.com/element-hq/element-web/releases/tag/v1.11.93) (2025-02-25)
==================================================================================================
## ✨ Features
* [backport] Dynamically load Element Web modules in Docker entrypoint ([#29358](https://github.com/element-hq/element-web/pull/29358)). Contributed by @t3chguy.
* ChangeRecoveryKey: error handling ([#29262](https://github.com/element-hq/element-web/pull/29262)). Contributed by @richvdh.
* Dehydration: enable dehydrated device on "Set up recovery" ([#29265](https://github.com/element-hq/element-web/pull/29265)). Contributed by @richvdh.
* Render reason for invite rejection. ([#29257](https://github.com/element-hq/element-web/pull/29257)). Contributed by @Half-Shot.
* New room list: add search section ([#29251](https://github.com/element-hq/element-web/pull/29251)). Contributed by @florianduros.
* New room list: hide favourites and people meta spaces ([#29241](https://github.com/element-hq/element-web/pull/29241)). Contributed by @florianduros.
* New Room List: Create new labs flag ([#29239](https://github.com/element-hq/element-web/pull/29239)). Contributed by @MidhunSureshR.
* Stop URl preview from covering message box ([#29215](https://github.com/element-hq/element-web/pull/29215)). Contributed by @edent.
* Rename "security key" into "recovery key" ([#29217](https://github.com/element-hq/element-web/pull/29217)). Contributed by @florianduros.
* Add new verification section to user profile ([#29200](https://github.com/element-hq/element-web/pull/29200)). Contributed by @MidhunSureshR.
* Initial support for runtime modules ([#29104](https://github.com/element-hq/element-web/pull/29104)). Contributed by @t3chguy.
* Add `Forgot recovery key?` button to encryption tab ([#29202](https://github.com/element-hq/element-web/pull/29202)). Contributed by @florianduros.
* Add KeyIcon to key storage out of sync toast ([#29201](https://github.com/element-hq/element-web/pull/29201)). Contributed by @florianduros.
* Improve rendering of empty topics in the timeline ([#29152](https://github.com/element-hq/element-web/pull/29152)). Contributed by @Half-Shot.
## 🐛 Bug Fixes
* Fix font scaling in member list ([#29285](https://github.com/element-hq/element-web/pull/29285)). Contributed by @florianduros.
* Grow member list search field when resizing the right panel ([#29267](https://github.com/element-hq/element-web/pull/29267)). Contributed by @langleyd.
* Don't reload roomview on offline connectivity check ([#29243](https://github.com/element-hq/element-web/pull/29243)). Contributed by @dbkr.
* Respect user's 12/24 hour preference consistently ([#29237](https://github.com/element-hq/element-web/pull/29237)). Contributed by @t3chguy.
* Restore the accessibility role on call views ([#29225](https://github.com/element-hq/element-web/pull/29225)). Contributed by @robintown.
* Revert `GoToHome` keyboard shortcut to `Ctrl``Shift``H` on macOS ([#28577](https://github.com/element-hq/element-web/pull/28577)). Contributed by @gy-mate.
* Encryption tab: display correct encryption panel when user cancels the reset identity flow ([#29216](https://github.com/element-hq/element-web/pull/29216)). Contributed by @florianduros.
Changes in [1.11.92](https://github.com/element-hq/element-web/releases/tag/v1.11.92) (2025-02-11)
==================================================================================================
## ✨ Features

View File

@@ -1,3 +1,5 @@
# syntax=docker.io/docker/dockerfile:1.14-labs
# Builder
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
@@ -8,7 +10,7 @@ ARG JS_SDK_BRANCH="master"
WORKDIR /src
COPY . /src
COPY --exclude=docker . /src
RUN /src/scripts/docker-link-repos.sh
RUN yarn --network-timeout=200000 install
RUN /src/scripts/docker-package.sh
@@ -19,11 +21,15 @@ RUN cp /src/config.sample.json /src/webapp/config.json
# App
FROM nginx:alpine-slim
# Install jq and moreutils for sponge, both used by our entrypoints
RUN apk add jq moreutils
COPY --from=builder /src/webapp /app
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
# through `envsubst` by the nginx docker image entry point.
COPY /docker/nginx-templates/* /etc/nginx/templates/
COPY /docker/docker-entrypoint.d/* /docker-entrypoint.d/
# Tell nginx to put its pidfile elsewhere, so it can run as non-root
RUN sed -i -e 's,/var/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf
@@ -40,3 +46,5 @@ USER nginx
# HTTP listen port
ENV ELEMENT_WEB_PORT=80
HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json

View File

@@ -31,5 +31,7 @@ module.exports = {
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
["@babel/plugin-proposal-decorators", { version: "2023-11" }], // only needed by the js-sdk
"@babel/plugin-transform-class-static-block", // only needed by the js-sdk for decorators
],
};

View File

@@ -0,0 +1,34 @@
#!/bin/sh
# Loads modules from `/modules` into config.json's `modules` field
set -e
entrypoint_log() {
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
echo "$@"
fi
}
# Copy these config files as a base
mkdir -p /tmp/element-web-config
cp /app/config*.json /tmp/element-web-config/
# If there are modules to be loaded
if [ -d "/modules" ]; then
cd /modules
for MODULE in *
do
# If the module has a package.json, use its main field as the entrypoint
ENTRYPOINT="index.js"
if [ -f "/modules/$MODULE/package.json" ]; then
ENTRYPOINT=$(jq -r '.main' "/modules/$MODULE/package.json")
fi
entrypoint_log "Loading module $MODULE with entrypoint $ENTRYPOINT"
# Append the module to the config
jq ".modules += [\"/modules/$MODULE/$ENTRYPOINT\"]" /tmp/element-web-config/config.json | sponge /tmp/element-web-config/config.json
done
fi

View File

@@ -18,8 +18,12 @@ server {
}
# covers config.json and config.hostname.json requests as it is prefix.
location /config {
root /tmp/element-web-config;
add_header Cache-Control "no-cache";
}
location /modules {
alias /modules;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;

67
docs/MVVM.md Normal file
View File

@@ -0,0 +1,67 @@
# MVVM
General description of the pattern can be found [here](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel). But the gist of it is that you divide your code into three sections:
1. Model: This is where the business logic and data resides.
2. View Model: This code exists to provide the logic necessary for the UI. It directly uses the Model code.
3. View: This is the UI code itself and depends on the view model.
If you do MVVM right, your view should be dumb i.e it gets data from the view model and merely displays it.
### Practical guidelines for MVVM in element-web
#### Model
This is anywhere your data or business logic comes from. If your view model is accessing something simple exposed from `matrix-js-sdk`, then the sdk is your model. If you're using something more high level in element-web to get your data/logic (eg: `MemberListStore`), then that becomes your model.
#### View Model
1. View model is always a custom react hook named like `useFooViewModel()`.
2. The return type of your view model (known as view state) must be defined as a typescript interface:
```ts
inteface FooViewState {
somethingUseful: string;
somethingElse: BarType;
update: () => Promise<void>
...
}
```
3. Any react state that your UI needs must be in the view model.
#### View
1. Views are simple react components (eg: `FooView`).
2. Views usually start by calling the view model hook, eg:
```tsx
const FooView: React.FC<IProps> = (props: IProps) => {
const vm = useFooViewModel();
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
3. Views are also allowed to accept the view model as a prop, eg:
```tsx
const FooView: React.FC<IProps> = ({ vm }: IProps) => {
....
return(
<div>
{vm.somethingUseful}
</div>
);
}
```
4. Multiple views can share the same view model if necessary.
### Benefits
1. MVVM forces a separation of concern i.e we will no longer have large react components that have a lot of state and rendering code mixed together. This improves code readability and makes it easier to introduce changes.
2. Introduces the possibility of code reuse. You can reuse an old view model with a new view or vice versa.
3. Adding to the point above, in future you could import element-web view models to your project and supply your own views thus creating something similar to the [hydrogen sdk](https://github.com/element-hq/hydrogen-web/blob/master/doc/SDK.md).
### Example
We started experimenting with MVVM in the redesigned memberlist, you can see the code [here](https://github.com/vector-im/element-web/blob/develop/src/components/views/rooms/MemberList/MemberListView.tsx).

View File

@@ -163,14 +163,14 @@ These two options describe the various availability for the application. When th
such as trying to get the user to use an Android app or the desktop app for encrypted search, the config options will be looked
at to see if the link should be to somewhere else.
Starting with `desktop_builds`, the following subproperties are available:
Starting with `desktop_builds`, the following sub-properties are available:
1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere.
2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels.
3. `url`: Required. The download URL for the app. This is used as a hyperlink.
4. `url_macos`: Optional. Direct link to download macOS desktop app.
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app.
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
6. `url_win64arm`: Optional. Direct link to download Windows ARM 64-bit desktop app.
7. `url_linux`: Optional. Direct link to download Linux desktop app.
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io

View File

@@ -66,6 +66,18 @@ on other runtimes may require root privileges. To resolve this, either run the
image as root (`docker run --user 0`) or, better, change the port that nginx
listens on via the `ELEMENT_WEB_PORT` environment variable.
[Element Web Modules](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api) can be dynamically loaded
by being made available (e.g. via bind mount) in a directory within `/modules/`.
The default entrypoint will be index.js in that directory but can be overridden if a package.json file is found with a `main` directive.
These modules will be presented in a `/modules` subdirectory within the webroot, and automatically added to the config.json `modules` field.
If you wish to use docker in read-only mode,
you should follow the [upstream instructions](https://hub.docker.com/_/nginx#:~:text=Running%20nginx%20in%20read%2Donly%20mode)
but additionally include the following directories:
- /tmp/
- /etc/nginx/conf.d/
The behaviour of the docker image can be customised via the following
environment variables:

View File

@@ -19,6 +19,7 @@ export default {
ignore: [
// Keep for now
"src/hooks/useLocalStorageState.ts",
"src/hooks/useTimeout.ts",
"src/components/views/elements/InfoTooltip.tsx",
"src/components/views/elements/StyledCheckbox.tsx",
],

View File

@@ -1,6 +1,6 @@
{
"name": "element-web",
"version": "1.11.93-rc.0",
"version": "1.11.95",
"description": "Element: the future of secure communication",
"author": "New Vector Ltd.",
"repository": {
@@ -62,19 +62,19 @@
"test": "jest",
"test:playwright": "playwright test",
"test:playwright:open": "yarn test:playwright --ui",
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
"test:playwright:screenshots:build": "docker build playwright -t element-web-playwright",
"test:playwright:screenshots:run": "docker run --rm --network host -e BASE_URL -e CI -v $(pwd):/work/ -v $(node -e 'console.log(require(`path`).dirname(require.resolve(`matrix-js-sdk/package.json`)))'):/work/node_modules/matrix-js-sdk -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/:/tmp/ -it element-web-playwright --grep @screenshot --project=Chrome",
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
"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"
},
"resolutions": {
"@playwright/test": "1.50.1",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
"oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001697",
"caniuse-lite": "1.0.30001701",
"testcontainers": "10.20.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
},
@@ -84,16 +84,16 @@
"@fontsource/inconsolata": "^5",
"@fontsource/inter": "^5",
"@formatjs/intl-segmenter": "^11.5.7",
"@matrix-org/analytics-events": "^0.29.0",
"@matrix-org/analytics-events": "^0.29.2",
"@matrix-org/emojibase-bindings": "^1.3.4",
"@matrix-org/react-sdk-module-api": "^2.4.0",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^8.0.0",
"@sentry/browser": "^9.0.0",
"@types/png-chunks-extract": "^1.0.2",
"@types/react-virtualized": "^9.21.30",
"@vector-im/compound-design-tokens": "^3.0.0",
"@vector-im/compound-web": "^7.6.1",
"@vector-im/matrix-wysiwyg": "2.38.0",
"@vector-im/compound-design-tokens": "^4.0.0",
"@vector-im/compound-web": "^7.7.2",
"@vector-im/matrix-wysiwyg": "2.38.2",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@@ -128,7 +128,7 @@
"maplibre-gl": "^5.0.0",
"matrix-encrypt-attachment": "^1.0.3",
"matrix-events-sdk": "0.0.1",
"matrix-js-sdk": "37.0.0-rc.0",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.10.0",
"memoize-one": "^6.0.0",
"mime": "^4.0.4",
@@ -138,7 +138,7 @@
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.157.2",
"qrcode": "1.5.4",
"re-resizable": "6.10.3",
"re-resizable": "6.11.2",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.0",
"react-blurhash": "^0.3.0",
@@ -158,13 +158,14 @@
"devDependencies": {
"@action-validator/cli": "^0.6.0",
"@action-validator/core": "^0.6.0",
"@axe-core/playwright": "^4.8.1",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/plugin-proposal-decorators": "^7.25.9",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-class-properties": "^7.12.1",
"@babel/plugin-transform-class-static-block": "^7.26.0",
"@babel/plugin-transform-logical-assignment-operators": "^7.20.7",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
"@babel/plugin-transform-numeric-separator": "^7.12.7",
@@ -176,13 +177,13 @@
"@babel/preset-typescript": "^7.12.7",
"@babel/runtime": "^7.12.5",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@element-hq/element-web-playwright-common": "^1.1.5",
"@peculiar/webcrypto": "^1.4.3",
"@playwright/test": "^1.40.1",
"@playwright/test": "^1.50.1",
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
"@sentry/webpack-plugin": "^3.0.0",
"@stylistic/eslint-plugin": "^3.0.0",
"@svgr/webpack": "^8.0.0",
"@testcontainers/postgresql": "^10.16.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
@@ -218,12 +219,12 @@
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"babel-jest": "^29.0.0",
"babel-loader": "^9.0.0",
"babel-loader": "^10.0.0",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"blob-polyfill": "^9.0.0",
"chokidar": "^4.0.0",
"concurrently": "^9.0.0",
"copy-webpack-plugin": "^12.0.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.38.1",
"cronstrue": "^2.41.0",
"css-loader": "^7.0.0",
@@ -257,13 +258,12 @@
"jsqr": "^1.4.0",
"knip": "^5.36.2",
"lint-staged": "^15.0.2",
"mailpit-api": "^1.0.5",
"matrix-web-i18n": "^3.2.1",
"mini-css-extract-plugin": "2.9.2",
"minimist": "^1.2.6",
"modernizr": "^3.12.0",
"node-fetch": "^2.6.7",
"playwright-core": "^1.45.1",
"playwright-core": "^1.51.0",
"postcss": "8.4.46",
"postcss-easings": "^4.0.0",
"postcss-hexrgba": "2.1.0",
@@ -274,21 +274,20 @@
"postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "3.4.2",
"prettier": "3.5.2",
"process": "^0.11.10",
"raw-loader": "^4.0.2",
"rimraf": "^6.0.0",
"semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"strip-ansi": "^7.1.0",
"stylelint": "^16.13.0",
"stylelint-config-standard": "^37.0.0",
"stylelint-scss": "^6.0.0",
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
"terser-webpack-plugin": "^5.3.9",
"testcontainers": "^10.16.0",
"testcontainers": "^10.20.0",
"ts-node": "^10.9.1",
"typescript": "5.7.3",
"typescript": "5.8.2",
"util": "^0.12.5",
"web-streams-polyfill": "^4.0.0",
"webpack": "^5.89.0",

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { defineConfig, devices } from "@playwright/test";
import { Options } from "./playwright/services";
import { type WorkerOptions } from "./playwright/services";
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
@@ -21,7 +21,7 @@ const chromeProject = {
},
};
export default defineConfig<Options>({
export default defineConfig<WorkerOptions>({
projects: [
{
name: "Chrome",
@@ -83,6 +83,7 @@ export default defineConfig<Options>({
url: `${baseURL}/config.json`,
reuseExistingServer: true,
timeout: (process.env.CI ? 30 : 120) * 1000,
stdout: "pipe",
},
testDir: "playwright/e2e",
outputDir: "playwright/test-results",

View File

@@ -1,12 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024 The Matrix.org Foundation C.I.C.
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 "playwright-core/lib/utils" {
// This type is not public in playwright-core utils
export function sanitizeForFilePath(filePath: string): string;
}

View File

@@ -1,9 +0,0 @@
FROM mcr.microsoft.com/playwright:v1.49.1-noble
WORKDIR /work
# fonts-dejavu is needed for the same RTL rendering as on CI
RUN apt-get update && apt-get -y install docker.io fonts-dejavu
COPY docker-entrypoint.sh /opt/docker-entrypoint.sh
ENTRYPOINT ["bash", "/opt/docker-entrypoint.sh"]

View File

@@ -1,5 +0,0 @@
#!/bin/bash
set -e
npx playwright test --update-snapshots --reporter line $@

View File

@@ -28,7 +28,7 @@ const checkDMRoom = async (page: Page) => {
};
const startDMWithBob = async (page: Page, bob: Bot) => {
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
await page.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
await expect(

View File

@@ -22,18 +22,6 @@ test.use({
msc3814_enabled: true,
},
},
config: async ({ config, context }, use) => {
const wellKnown = {
...config.default_server_config,
"org.matrix.msc3814": true,
};
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
await route.fulfill({ json: wellKnown });
});
await use(config);
},
});
test.describe("Dehydration", () => {
@@ -116,6 +104,40 @@ test.describe("Dehydration", () => {
expect(dehydratedDeviceIds.length).toBe(1);
expect(dehydratedDeviceIds[0]).not.toEqual(initialDehydratedDeviceIds[0]);
});
test("'Reset cryptographic identity' removes dehydrated device", async ({ page, homeserver, app, credentials }) => {
await logIntoElement(page, credentials);
// Create a dehydrated device by setting up recovery (see "'Set up
// recovery' creates dehydrated device" test above)
const settingsDialogLocator = await app.settings.openUserSettings("Encryption");
await settingsDialogLocator.getByRole("button", { name: "Set up recovery" }).click();
// First it displays an informative panel about the recovery key
await expect(settingsDialogLocator.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
// Next, it displays the new recovery key. We click on the copy button.
await expect(settingsDialogLocator.getByText("Save your recovery key somewhere safe")).toBeVisible();
await settingsDialogLocator.getByRole("button", { name: "Copy" }).click();
const recoveryKey = await app.getClipboard();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expect(
settingsDialogLocator.getByText("Enter your recovery key to confirm", { exact: true }),
).toBeVisible();
await settingsDialogLocator.getByRole("textbox").fill(recoveryKey);
await settingsDialogLocator.getByRole("button", { name: "Finish set up" }).click();
await expectDehydratedDeviceEnabled(app);
// After recovery is set up, we reset our cryptographic identity, which
// should drop the dehydrated device.
await settingsDialogLocator.getByRole("button", { name: "Reset cryptographic identity" }).click();
await settingsDialogLocator.getByRole("button", { name: "Continue" }).click();
await expectDehydratedDeviceDisabled(app);
});
});
async function getDehydratedDeviceIds(client: Client): Promise<string[]> {
@@ -144,3 +166,16 @@ async function expectDehydratedDeviceEnabled(app: ElementAppPage): Promise<void>
})
.toEqual(1);
}
/** Wait for our user to not have a dehydrated device */
async function expectDehydratedDeviceDisabled(app: ElementAppPage): Promise<void> {
// It might be nice to do this via the UI, but currently this info is not exposed via the UI.
//
// Note we might have to wait for the device list to be refreshed, so we wrap in `expect.poll`.
await expect
.poll(async () => {
const dehydratedDeviceIds = await getDehydratedDeviceIds(app.client);
return dehydratedDeviceIds.length;
})
.toEqual(0);
}

View File

@@ -21,6 +21,7 @@ import {
waitForVerificationRequest,
} from "./utils";
import { type Bot } from "../../pages/bot";
import { Toasts } from "../../pages/toasts.ts";
test.describe("Device verification", { tag: "@no-webkit" }, () => {
let aliceBotClient: Bot;
@@ -72,6 +73,51 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
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 }) => {
// Before we log in, the bot creates an encrypted room, so that we can test the toast behaviour that only happens
// when we are in an encrypted room.
await aliceBotClient.createRoom({
initial_state: [
{
type: "m.room.encryption",
state_key: "",
content: { algorithm: "m.megolm.v1.aes-sha2" },
},
],
});
// In order to simulate a real environment more accurately, we need to slow down the arrival of the
// `m.secret.send` to-device messages. That's slightly tricky to do directly, so instead we delay the *outgoing*
// `m.secret.request` messages.
await page.route("**/_matrix/client/v3/sendToDevice/m.secret.request/**", async (route) => {
await route.fulfill({ json: {} });
await new Promise((f) => setTimeout(f, 1000));
await route.fetch();
});
await logIntoElement(page, credentials);
// 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 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();
// There should be no toast (other than the notifications one)
const toasts = new Toasts(page);
await toasts.rejectToast("Notifications");
await toasts.assertNoToasts();
});
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
await logIntoElement(page, credentials);

View File

@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { type APIRequestContext } from "playwright-core";
import { type APIRequestContext } from "@playwright/test";
import { type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import { ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
import { type HomeserverInstance } from "../plugins/homeserver";
import { ClientServerApi } from "../plugins/utils/api.ts";
/**
* A small subset of the Client-Server API used to manipulate the state of the

View File

@@ -267,7 +267,6 @@ test.describe("Editing", () => {
app,
room,
axe,
checkA11y,
}) => {
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
@@ -282,7 +281,7 @@ test.describe("Editing", () => {
const line = tile.locator(".mx_EventTile_line");
await line.hover();
await line.getByRole("button", { name: "Edit" }).click();
await checkA11y();
await expect(axe).toHaveNoViolations();
const editComposer = page.getByRole("textbox", { name: "Edit message" });
await editComposer.pressSequentially("Foo");
await editComposer.press("Backspace");
@@ -290,7 +289,7 @@ test.describe("Editing", () => {
await editComposer.press("Backspace");
await editComposer.press("Enter");
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
await checkA11y();
await expect(axe).toHaveNoViolations();
}
await expect(
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
@@ -305,7 +304,6 @@ test.describe("Editing", () => {
user,
app,
axe,
checkA11y,
bot: bob,
}) => {
// This tests the behaviour when a message has been edited some time after it has been sent, and we

View File

@@ -6,22 +6,21 @@ 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 { expect, test as base } from "../../element-web-test";
import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
import { selectHomeserver } from "../utils";
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
import { isDendrite } from "../../plugins/homeserver/dendrite";
import { type Credentials } from "../../plugins/homeserver";
const email = "user@nowhere.dummy";
const test = base.extend<{ credentials: Pick<Credentials, "username" | "password"> }>({
const test = base.extend({
// eslint-disable-next-line no-empty-pattern
credentials: async ({}, use, testInfo) => {
await use({
username: `user_${testInfo.testId}`,
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
password: "oETo7MPf0o",
});
} as CredentialsWithDisplayName);
},
});

View File

@@ -77,7 +77,7 @@ test.describe("Invite dialog", function () {
"should support inviting a user to Direct Messages",
{ tag: "@screenshot" },
async ({ page, app, user, bot }) => {
await page.locator(".mx_RoomList").getByRole("button", { name: "Start chat" }).click();
await page.locator(".mx_LegacyRoomList").getByRole("button", { name: "Start chat" }).click();
const other = page.locator(".mx_InviteDialog_other");
// Assert that the header is rendered

View File

@@ -0,0 +1,88 @@
/*
* 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 { expect, test } from "../../../element-web-test";
import type { Page } from "@playwright/test";
test.describe("Room list filters and sort", () => {
test.use({
displayName: "Alice",
botCreateOpts: {
displayName: "BotBob",
autoAcceptInvites: true,
},
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
function getPrimaryFilters(page: Page) {
return page.getByRole("listbox", { name: "Room list filters" });
}
test.beforeEach(async ({ page, app, bot, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
await app.client.createRoom({ name: "empty room" });
const unReadDmId = await bot.createRoom({
name: "unread dm",
invite: [user.userId],
is_direct: true,
});
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
const unReadRoomId = await app.client.createRoom({ name: "unread room" });
await app.client.inviteUser(unReadRoomId, bot.credentials.userId);
await bot.joinRoom(unReadRoomId);
await bot.sendMessage(unReadRoomId, "I am a robot. Beep.");
const favouriteId = await app.client.createRoom({ name: "favourite room" });
await app.client.evaluate(async (client, favouriteId) => {
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
}, favouriteId);
});
test("should filter the list (with primary filters)", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomList = getRoomList(page);
const primaryFilters = getPrimaryFilters(page);
const allFilters = await primaryFilters.locator("option").all();
for (const filter of allFilters) {
expect(await filter.getAttribute("aria-selected")).toBe("false");
}
await expect(primaryFilters).toMatchScreenshot("unselected-primary-filters.png");
await primaryFilters.getByRole("option", { name: "Unread" }).click();
// 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(2);
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();
expect(await roomList.locator("role=gridcell").count()).toBe(1);
await primaryFilters.getByRole("option", { name: "Rooms" }).click();
await expect(roomList.getByRole("gridcell", { name: "unread room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
await expect(roomList.getByRole("gridcell", { name: "empty room" })).toBeVisible();
expect(await roomList.locator("role=gridcell").count()).toBe(3);
});
});

View File

@@ -0,0 +1,87 @@
/*
* 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 type { Page } from "@playwright/test";
test.describe("Header section of the room list", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
/**
* Get the header section of the room list
* @param page
*/
function getHeaderSection(page: Page) {
return page.getByTestId("room-list-header");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
});
test("should render the header section", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListHeader = getHeaderSection(page);
await expect(roomListHeader).toMatchScreenshot("room-list-header.png");
const composeMenu = roomListHeader.getByRole("button", { name: "Add" });
await composeMenu.click();
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();
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
await app.closeDialog();
// New room should open the room creation dialog
await composeMenu.click();
await page.getByRole("menuitem", { name: "New room" }).click();
await expect(page.getByRole("heading", { name: "Create a private room" })).toBeVisible();
await app.closeDialog();
});
test("should render the header section for a space", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createSpace({ name: "MySpace" });
await page.getByRole("button", { name: "MySpace" }).click();
const roomListHeader = getHeaderSection(page);
await expect(roomListHeader).toMatchScreenshot("room-list-space-header.png");
await expect(roomListHeader.getByRole("heading", { name: "MySpace" })).toBeVisible();
await expect(roomListHeader.getByRole("button", { name: "Add" })).toBeVisible();
const spaceMenu = roomListHeader.getByRole("button", { name: "Open space menu" });
await spaceMenu.click();
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-space-menu.png");
// It should open the space home
await page.getByRole("menuitem", { name: "Space home" }).click();
await expect(page.getByRole("main").getByRole("heading", { name: "MySpace" })).toBeVisible();
// It should open the invite dialog
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to MySpace" })).toBeVisible();
await app.closeDialog();
// It should open the space preferences
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Preferences" }).click();
await expect(page.getByRole("heading", { name: "Preferences" })).toBeVisible();
await app.closeDialog();
// It should open the space settings
await spaceMenu.click();
await page.getByRole("menuitem", { name: "Space Settings" }).click();
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
await app.closeDialog();
});
});

View File

@@ -9,7 +9,7 @@ import { type Page } from "@playwright/test";
import { test, expect } from "../../../element-web-test";
test.describe("Search section of the room list", () => {
test.describe("Room list panel", () => {
test.use({
labsFlags: ["feature_new_room_list"],
});
@@ -19,16 +19,23 @@ test.describe("Search section of the room list", () => {
* @param page
*/
function getRoomListView(page: Page) {
return page.getByTestId("room-list-view");
return page.getByTestId("room-list-panel");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
// Populate the room list
for (let i = 0; i < 20; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});
test("should render the room list view", { tag: "@screenshot" }, async ({ page, app, user }) => {
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomListView(page);
await expect(roomListView).toMatchScreenshot("room-list-view.png");
// Wait for the last room to be visible
await expect(roomListView.getByRole("gridcell", { name: "Open room room19" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list-panel.png");
});
});

View File

@@ -0,0 +1,80 @@
/*
* 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 { test, expect } from "../../../element-web-test";
test.describe("Room list", () => {
test.use({
displayName: "Alice",
labsFlags: ["feature_new_room_list"],
});
/**
* Get the room list
* @param page
*/
function getRoomList(page: Page) {
return page.getByTestId("room-list");
}
test.beforeEach(async ({ page, app, user }) => {
// The notification toast is displayed above the search section
await app.closeNotificationToast();
for (let i = 0; i < 30; i++) {
await app.client.createRoom({ name: `room${i}` });
}
});
test("should render the room list", { tag: "@screenshot" }, async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await expect(roomListView.getByRole("gridcell", { name: "Open room room29" })).toBeVisible();
await expect(roomListView).toMatchScreenshot("room-list.png");
await roomListView.hover();
// 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 expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
});
test("should open the room when it is clicked", async ({ page, app, user }) => {
const roomListView = getRoomList(page);
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
await expect(page.getByRole("heading", { name: "room29", level: 1 })).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" });
await roomItem.hover();
await expect(roomItem).toMatchScreenshot("room-list-item-hover.png");
const roomItemMenu = roomItem.getByRole("button", { name: "More Options" });
await roomItemMenu.click();
await expect(page).toMatchScreenshot("room-list-item-open-more-options.png");
// It should make the room favourited
await page.getByRole("menuitemcheckbox", { name: "Favourited" }).click();
// Check that the room is favourited
await roomItem.hover();
await roomItemMenu.click();
await expect(page.getByRole("menuitemcheckbox", { name: "Favourited" })).toBeChecked();
// It should show the invite dialog
await page.getByRole("menuitem", { name: "invite" }).click();
await expect(page.getByRole("heading", { name: "Invite to room29" })).toBeVisible();
await app.closeDialog();
// It should leave the room
await roomItem.hover();
await roomItemMenu.click();
await page.getByRole("menuitem", { name: "leave room" }).click();
await expect(roomItem).not.toBeVisible();
});
});

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "playwright-core";
import { type Page } from "@playwright/test";
import { expect, test } from "../../element-web-test";
import { selectHomeserver } from "../utils";
@@ -120,7 +120,7 @@ test.describe("Login", () => {
credentials,
page,
homeserver,
checkA11y,
axe,
}) => {
await page.goto("/");
@@ -149,7 +149,7 @@ test.describe("Login", () => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
// cy.percySnapshot("Login");
await checkA11y();
await expect(axe).toHaveNoViolations();
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
await page.getByPlaceholder("Password").fill(credentials.password);

View File

@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
/* See readme.md for tips on writing these tests. */
import { type Locator, type Page } from "playwright-core";
import { type Locator, type Page } from "@playwright/test";
import { test, expect } from "../../element-web-test";

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type MailpitClient } from "mailpit-api";
import { type MailpitClient } from "@element-hq/element-web-playwright-common/lib/testcontainers";
import { type Page } from "@playwright/test";
import { expect } from "../../element-web-test";

View File

@@ -34,7 +34,7 @@ test.describe("Email Registration", async () => {
test(
"registers an account and lands on the home page",
{ tag: "@screenshot" },
async ({ page, mailpitClient, request, checkA11y }) => {
async ({ page, mailpitClient, request, axe }) => {
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
// Hide the server text as it contains the randomly allocated Homeserver port
const screenshotOptions = { mask: [page.locator(".mx_ServerPicker_server")] };
@@ -47,7 +47,7 @@ test.describe("Email Registration", async () => {
await expect(page.getByText("Check your email to continue")).toBeVisible();
await expect(page).toMatchScreenshot("registration_check_your_email.png", screenshotOptions);
await checkA11y();
await expect(axe).toHaveNoViolations();
await expect(page.getByText("An error was encountered when sending the email")).not.toBeVisible();

View File

@@ -33,12 +33,12 @@ test.describe("Registration", () => {
test(
"registers an account and lands on the home screen",
{ tag: "@screenshot" },
async ({ homeserver, page, checkA11y, crypto }) => {
async ({ homeserver, page, axe, crypto }) => {
await page.getByRole("button", { name: "Edit", exact: true }).click();
await expect(page.getByRole("button", { name: "Continue", exact: true })).toBeVisible();
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("server-picker.png");
await checkA11y();
await expect(axe).toHaveNoViolations();
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.baseUrl);
await page.getByRole("button", { name: "Continue", exact: true }).click();
@@ -52,7 +52,7 @@ test.describe("Registration", () => {
includeDialogBackground: true,
};
await expect(page).toMatchScreenshot("registration.png", screenshotOptions);
await checkA11y();
await expect(axe).toHaveNoViolations();
await page.getByRole("textbox", { name: "Username", exact: true }).fill("alice");
await page.getByPlaceholder("Password", { exact: true }).fill("totally a great password");
@@ -62,12 +62,12 @@ test.describe("Registration", () => {
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(page).toMatchScreenshot("email-prompt.png", screenshotOptions);
await checkA11y();
await expect(axe).toHaveNoViolations();
await dialog.getByRole("button", { name: "Continue", exact: true }).click();
await expect(page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy")).toBeVisible();
await expect(page).toMatchScreenshot("terms-prompt.png", screenshotOptions);
await checkA11y();
await expect(axe).toHaveNoViolations();
const termsPolicy = page.locator(".mx_InteractiveAuthEntryComponents_termsPolicy");
await termsPolicy.getByRole("checkbox").click(); // Click the checkbox before terms of service anchor link

View File

@@ -67,6 +67,15 @@ test.describe("RightPanel", () => {
},
);
test("should have padding under leave room", { tag: "@screenshot" }, async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);
const leaveButton = await page.getByRole("menuitem", { name: "Leave Room" });
await leaveButton.scrollIntoViewIfNeeded();
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("with-leave-room.png");
});
test("should handle clicking add widgets", async ({ page, app }) => {
await viewRoomSummaryByName(page, app, ROOM_NAME);

View File

@@ -7,6 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import { type Page } from "@playwright/test";
import { type Visibility } from "matrix-js-sdk/src/matrix";
import { test, expect } from "../../element-web-test";
import { type ElementAppPage } from "../../pages/ElementAppPage";
@@ -85,6 +86,15 @@ test.describe("Room Header", () => {
await expect(header).toMatchScreenshot("room-header-long-name.png");
},
);
test("should render room header icon correctly", { tag: "@screenshot" }, async ({ page, app, user }) => {
await app.client.createRoom({ name: "Test Room", visibility: "public" as Visibility });
await app.viewRoomByName("Test Room");
const header = page.locator(".mx_RoomHeader");
await expect(header).toMatchScreenshot("room-header-with-icon.png");
});
});
test.describe("with a video room", () => {

View File

@@ -17,9 +17,7 @@ import {
} from "../../crypto/utils";
test.describe("Encryption tab", () => {
test.use({
displayName: "Alice",
});
test.use({ displayName: "Alice" });
let recoveryKey: GeneratedSecretStorageKey;
let expectedBackupVersion: string;
@@ -111,4 +109,36 @@ test.describe("Encryption tab", () => {
// The user is prompted to reset their identity
await expect(dialog.getByText("Forgot your recovery key? Youll need to reset your identity.")).toBeVisible();
});
test("should warn before turning off key storage", { tag: "@screenshot" }, async ({ page, app, util }) => {
await verifySession(app, recoveryKey.encodedPrivateKey);
await util.openEncryptionTab();
await page.getByRole("checkbox", { name: "Allow key storage" }).click();
await expect(
page.getByRole("heading", { name: "Are you sure you want to turn off key storage and delete it?" }),
).toBeVisible();
await expect(util.getEncryptionTabContent()).toMatchScreenshot("delete-key-storage-confirm.png");
const deleteRequestPromises = [
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.master")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.self_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.cross_signing.user_signing")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.megolm_backup.v1")),
page.waitForRequest((req) => req.url().endsWith("/account_data/m.secret_storage.default_key")),
page.waitForRequest((req) => req.url().includes("/account_data/m.secret_storage.key.")),
];
await page.getByRole("button", { name: "Delete key storage" }).click();
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).not.toBeChecked();
for (const prom of deleteRequestPromises) {
const request = await prom;
expect(request.method()).toBe("PUT");
expect(request.postData()).toBe(JSON.stringify({}));
}
});
});

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 { test, expect } from "../../element-web-test";
test.describe("Quick settings menu", () => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
await page.getByRole("button", { name: "Quick settings" }).click();
// Assert that the top heading is renderedc
const settings = page.getByTestId("quick-settings-menu");
await expect(settings).toBeVisible();
await expect(settings).toMatchScreenshot("quick-settings.png");
});
});

View File

@@ -32,7 +32,7 @@ test.describe("Security user settings tab", () => {
});
test.describe("AnalyticsLearnMoreDialog", () => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page }) => {
test("should be rendered properly", { tag: "@screenshot" }, async ({ app, page, user }) => {
const tab = await app.settings.openUserSettings("Security");
await tab.getByRole("button", { name: "Learn more" }).click();
await expect(page.locator(".mx_AnalyticsLearnMoreDialog_wrapper .mx_Dialog")).toMatchScreenshot(
@@ -41,16 +41,57 @@ test.describe("Security user settings tab", () => {
});
});
test("should contain section to set ID server", async ({ app }) => {
test("should be able to set an ID server", async ({ app, context, user, page }) => {
const tab = await app.settings.openUserSettings("Security");
const setIdServer = tab.locator(".mx_SetIdServer");
await context.route("https://identity.example.org/_matrix/identity/v2", async (route) => {
await route.fulfill({
status: 200,
json: {},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/account/register", async (route) => {
await route.fulfill({
status: 200,
json: {
token: "AToken",
},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/account", async (route) => {
await route.fulfill({
status: 200,
json: {
user_id: user.userId,
},
});
});
await context.route("https://identity.example.org/_matrix/identity/v2/terms", async (route) => {
await route.fulfill({
status: 200,
json: {
policies: {},
},
});
});
const setIdServer = tab.locator(".mx_IdentityServerPicker");
await setIdServer.scrollIntoViewIfNeeded();
// Assert that an input area for identity server exists
await expect(setIdServer.getByRole("textbox", { name: "Enter a new identity server" })).toBeVisible();
const textElement = setIdServer.getByRole("textbox", { name: "Enter a new identity server" });
await textElement.click();
await textElement.fill("https://identity.example.org");
await setIdServer.getByRole("button", { name: "Change" }).click();
await expect(setIdServer.getByText("Checking server")).toBeVisible();
// Accept terms
await page.getByTestId("dialog-primary-button").click();
// Check identity has changed.
await expect(setIdServer.getByText("Your identity server has been changed")).toBeVisible();
// Ensure section title is updated.
await expect(tab.getByText(`Identity server (identity.example.org)`, { exact: true })).toBeVisible();
});
test("should enable show integrations as enabled", async ({ app, page }) => {
test("should show integrations as enabled", async ({ app, page, user }) => {
const tab = await app.settings.openUserSettings("Security");
const setIntegrationManager = tab.locator(".mx_SetIntegrationManager");
@@ -61,7 +102,9 @@ test.describe("Security user settings tab", () => {
}),
).toBeVisible();
// Make sure integration manager's toggle switch is enabled
await expect(setIntegrationManager.locator(".mx_ToggleSwitch_enabled")).toBeVisible();
const toggleswitch = setIntegrationManager.getByLabel("Enable the integration manager");
await expect(toggleswitch).toBeVisible();
await expect(toggleswitch).toBeChecked();
await expect(setIntegrationManager.locator(".mx_SetIntegrationManager_heading_manager")).toHaveText(
"Manage integrations(scalar.vector.im)",
);

View File

@@ -227,7 +227,7 @@ test.describe("Spaces", () => {
test(
"should render subspaces in the space panel only when expanded",
{ tag: "@screenshot" },
async ({ page, app, user, axe, checkA11y }) => {
async ({ page, app, user, axe }) => {
axe.disableRules([
// Disable this check as it triggers on nested roving tab index elements which are in practice fine
"nested-interactive",
@@ -249,7 +249,7 @@ test.describe("Spaces", () => {
await expect(spaceTree.getByRole("button", { name: "Root Space" })).toBeVisible();
await expect(spaceTree.getByRole("button", { name: "Child Space" })).not.toBeVisible();
await checkA11y();
await expect(axe).toHaveNoViolations();
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-collapsed.png");
// This finds the expand button with the class name "mx_SpaceButton_toggleCollapse". Note there is another
@@ -261,7 +261,7 @@ test.describe("Spaces", () => {
await expect(item).toBeVisible();
await expect(item.locator(".mx_SpaceItem", { hasText: "Child Space" })).toBeVisible();
await checkA11y();
await expect(axe).toHaveNoViolations();
await expect(page.locator(".mx_SpacePanel")).toMatchScreenshot("space-panel-expanded.png");
},
);

View File

@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
@@ -277,7 +277,7 @@ test.describe("Timeline", () => {
test(
"should add inline start margin to an event line on IRC layout",
{ tag: "@screenshot" },
async ({ page, app, room, axe, checkA11y }) => {
async ({ page, app, room, axe }) => {
axe.disableRules("color-contrast");
await page.goto(`/#/room/${room.roomId}`);
@@ -318,7 +318,7 @@ test.describe("Timeline", () => {
`,
},
);
await checkA11y();
await expect(axe).toHaveNoViolations();
},
);
});
@@ -743,68 +743,64 @@ test.describe("Timeline", () => {
).toBeVisible();
});
test(
"should render url previews",
{ tag: "@screenshot" },
async ({ page, app, room, axe, checkA11y, context }) => {
axe.disableRules("color-contrast");
test("should render url previews", { tag: "@screenshot" }, async ({ page, app, room, axe, context }) => {
axe.disableRules("color-contrast");
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
await context.route(
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
async (route) => {
await route.fulfill({
path: "playwright/sample-files/riot.png",
});
},
);
await page.route(
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
async (route) => {
await route.fulfill({
json: {
"og:title": "Element Call",
"og:description": null,
"og:image:width": 48,
"og:image:height": 48,
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
"og:image:type": "image/png",
"matrix:image:size": 2121,
},
});
},
);
// Element Web uses a Service Worker to rewrite unauthenticated media requests to authenticated ones, but
// the page can't see this happening. We intercept the route at the BrowserContext to ensure we get it
// post-worker, but we can't waitForResponse on that, so the page context is still used there. Because
// the page doesn't see the rewrite, it waits for the unauthenticated route. This is only confusing until
// the js-sdk (and thus the app as a whole) switches to using authenticated endpoints by default, hopefully.
await context.route(
"**/_matrix/client/v1/media/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*",
async (route) => {
await route.fulfill({
path: "playwright/sample-files/riot.png",
});
},
);
await page.route(
"**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*",
async (route) => {
await route.fulfill({
json: {
"og:title": "Element Call",
"og:description": null,
"og:image:width": 48,
"og:image:height": 48,
"og:image": "mxc://matrix.org/2022-08-16_yaiSVSRIsNFfxDnV",
"og:image:type": "image/png",
"matrix:image:size": 2121,
},
});
},
);
const requestPromises: Promise<any>[] = [
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
// see context.route above for why we listen for the unauthenticated endpoint
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
];
const requestPromises: Promise<any>[] = [
page.waitForResponse("**/_matrix/media/v3/preview_url?url=https%3A%2F%2Fcall.element.io%2F&ts=*"),
// see context.route above for why we listen for the unauthenticated endpoint
page.waitForResponse("**/_matrix/media/v3/thumbnail/matrix.org/2022-08-16_yaiSVSRIsNFfxDnV?*"),
];
await app.client.sendMessage(room.roomId, "https://call.element.io/");
await page.goto(`/#/room/${room.roomId}`);
await app.client.sendMessage(room.roomId, "https://call.element.io/");
await page.goto(`/#/room/${room.roomId}`);
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
await Promise.all(requestPromises);
await expect(page.locator(".mx_LinkPreviewWidget").getByText("Element Call")).toBeVisible();
await Promise.all(requestPromises);
await checkA11y();
await expect(axe).toHaveNoViolations();
await app.timeline.scrollToBottom();
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
await app.timeline.scrollToBottom();
await expect(page.locator(".mx_EventTile_last")).toMatchScreenshot("url-preview.png", {
// Exclude timestamp and read marker from snapshot
mask: [page.locator(".mx_MessageTimestamp")],
css: `
.mx_TopUnreadMessagesBar, .mx_MessagePanel_myReadMarker {
display: none !important;
}
`,
});
},
);
});
});
test.describe("on search results panel", () => {
test(
@@ -875,6 +871,53 @@ test.describe("Timeline", () => {
);
});
});
test("should render a code block", { tag: "@screenshot" }, async ({ page, app, room }) => {
await page.goto(`/#/room/${room.roomId}`);
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
await expect(
page
.locator(".mx_GenericEventListSummary_summary")
.getByText(`${OLD_NAME} created and configured the room.`),
).toBeVisible();
// Send a code block
const composer = app.getComposerField();
await composer.fill("```\nconsole.log('Hello, world!');\n```");
await composer.press("Enter");
const tile = page.locator(".mx_EventTile");
await expect(tile).toBeVisible();
await expect(tile).toMatchScreenshot("code-block.png", { mask: [page.locator(".mx_MessageTimestamp")] });
// Edit a code block and assert the edited code block has been correctly rendered
await tile.hover();
await page.getByRole("toolbar", { name: "Message Actions" }).getByRole("button", { name: "Edit" }).click();
await page
.getByRole("textbox", { name: "Edit message" })
.fill("```\nconsole.log('Edited: Hello, world!');\n```");
await page.getByRole("textbox", { name: "Edit message" }).press("Enter");
const newTile = page.locator(".mx_EventTile");
await expect(newTile).toMatchScreenshot("edited-code-block.png", {
mask: [page.locator(".mx_MessageTimestamp")],
});
});
test("should be able to hide an image", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);
await sendImage(app.client, room.roomId, NEW_AVATAR);
await app.timeline.scrollToBottom();
const imgTile = page.locator(".mx_MImageBody").first();
await expect(imgTile).toBeVisible();
await imgTile.hover();
await page.getByRole("button", { name: "Hide" }).click();
// Check that the image is now hidden.
await expect(page.getByRole("link", { name: "Show image" })).toBeVisible();
});
});
test.describe("message sending", { tag: ["@no-firefox", "@no-webkit"] }, () => {

View File

@@ -7,18 +7,18 @@ Please see LICENSE files in the repository root for full details.
*/
import {
expect as baseExpect,
type Locator,
type Page,
type ExpectMatcherState,
type ElementHandle,
type MatcherReturnType,
type Page,
type Locator,
type PlaywrightTestArgs,
type Fixtures as _Fixtures,
} from "@playwright/test";
import { sanitizeForFilePath } from "playwright-core/lib/utils";
import AxeBuilder from "@axe-core/playwright";
import _ from "lodash";
import { extname } from "node:path";
import {
type TestFixtures as BaseTestFixtures,
expect as baseExpect,
type ToMatchScreenshotOptions,
} from "@element-hq/element-web-playwright-common";
import type { IConfigOptions } from "../src/IConfigOptions";
import { type Credentials } from "./plugins/homeserver";
@@ -27,71 +27,22 @@ import { Crypto } from "./pages/crypto";
import { Toasts } from "./pages/toasts";
import { Bot, type CreateBotOpts } from "./pages/bot";
import { Webserver } from "./plugins/webserver";
import { type Options, type Services, test as base } from "./services.ts";
import { type WorkerOptions, type Services, test as base } from "./services";
// Enable experimental service worker support
// See https://playwright.dev/docs/service-workers-experimental#how-to-enable
process.env["PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS"] = "1";
// This is deliberately quite a minimal config.json, so that we can test that the default settings actually work.
const CONFIG_JSON: Partial<IConfigOptions> = {
// The default language is set here for test consistency
setting_defaults: {
language: "en-GB",
},
// the location tests want a map style url.
map_style_url: "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx",
features: {
// We don't want to go through the feature announcement during the e2e test
feature_release_announcement: false,
},
};
declare module "@element-hq/element-web-playwright-common" {
// Improve the type for the config fixture based on the real type
export interface Config extends Omit<IConfigOptions, "default_server_config"> {}
}
export interface CredentialsWithDisplayName extends Credentials {
displayName: string;
}
export interface TestFixtures {
axe: AxeBuilder;
checkA11y: () => Promise<void>;
/**
* The contents of the config.json to send when the client requests it.
*/
config: typeof CONFIG_JSON;
/**
* The displayname to use for the user registered in {@link #credentials}.
*
* To set it, call `test.use({ displayName: "myDisplayName" })` in the test file or `describe` block.
* See {@link https://playwright.dev/docs/api/class-test#test-use}.
*/
displayName?: string;
/**
* A test fixture which registers a test user on the {@link #homeserver} and supplies the details
* of the registered user.
*/
credentials: CredentialsWithDisplayName;
/**
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
* but adds an initScript which will populate localStorage with the user's details from
* {@link #credentials} and {@link #homeserver}.
*
* Similar to {@link #user}, but doesn't load the app.
*/
pageWithCredentials: Page;
/**
* A (rather poorly-named) test fixture which registers a user per {@link #credentials}, stores
* the credentials into localStorage per {@link #homeserver}, and then loads the front page of the
* app.
*/
user: CredentialsWithDisplayName;
export interface TestFixtures extends BaseTestFixtures {
/**
* The same as {@link https://playwright.dev/docs/api/class-fixtures#fixtures-page|`page`},
* but wraps the returned `Page` in a class of utilities for interacting with the Element-Web UI,
@@ -105,13 +56,11 @@ export interface TestFixtures {
uut?: Locator; // Unit Under Test, useful place to refer a prepared locator
botCreateOpts: CreateBotOpts;
bot: Bot;
labsFlags: string[];
webserver: Webserver;
disablePresence: boolean;
}
type CombinedTestFixtures = PlaywrightTestArgs & TestFixtures;
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & Options, CombinedTestFixtures>;
export type Fixtures = _Fixtures<CombinedTestFixtures, Services & WorkerOptions, CombinedTestFixtures>;
export const test = base.extend<TestFixtures>({
context: async ({ context }, use, testInfo) => {
// We skip tests instead of using grep-invert to still surface the counts in the html report
@@ -121,102 +70,12 @@ export const test = base.extend<TestFixtures>({
);
await use(context);
},
disablePresence: false,
config: {}, // We merge this atop the default CONFIG_JSON in the page fixture to make extending it easier
page: async ({ homeserver, context, page, config, labsFlags, disablePresence }, use) => {
await context.route(`http://localhost:8080/config.json*`, async (route) => {
const json = {
...CONFIG_JSON,
...config,
default_server_config: {
"m.homeserver": {
base_url: homeserver.baseUrl,
},
...config.default_server_config,
},
};
json["features"] = {
...json["features"],
// Enable the lab features
...labsFlags.reduce((obj, flag) => {
obj[flag] = true;
return obj;
}, {}),
};
if (disablePresence) {
json["enable_presence_by_hs_url"] = {
[homeserver.baseUrl]: false,
};
}
await route.fulfill({ json });
});
await use(page);
axe: async ({ axe }, use) => {
// Exclude floating UI for now
await use(axe.exclude("[data-floating-ui-portal]"));
},
displayName: undefined,
credentials: async ({ context, homeserver, displayName: testDisplayName }, use, testInfo) => {
const names = ["Alice", "Bob", "Charlie", "Daniel", "Eve", "Frank", "Grace", "Hannah", "Isaac", "Judy"];
const password = _.uniqueId("password_");
const displayName = testDisplayName ?? _.sample(names)!;
const credentials = await homeserver.registerUser(`user_${testInfo.testId}`, password, displayName);
console.log(`Registered test user ${credentials.userId} with displayname ${displayName}`);
await use({
...credentials,
displayName,
});
},
labsFlags: [],
pageWithCredentials: async ({ page, homeserver, credentials }, use) => {
await page.addInitScript(
({ baseUrl, credentials }) => {
// Seed the localStorage with the required credentials
window.localStorage.setItem("mx_hs_url", baseUrl);
window.localStorage.setItem("mx_user_id", credentials.userId);
window.localStorage.setItem("mx_access_token", credentials.accessToken);
window.localStorage.setItem("mx_device_id", credentials.deviceId);
window.localStorage.setItem("mx_is_guest", "false");
window.localStorage.setItem("mx_has_pickle_key", "false");
window.localStorage.setItem("mx_has_access_token", "true");
window.localStorage.setItem(
"mx_local_settings",
JSON.stringify({
// Retain any other settings which may have already been set
...JSON.parse(window.localStorage.getItem("mx_local_settings") || "{}"),
// Ensure the language is set to a consistent value
language: "en",
}),
);
},
{ baseUrl: homeserver.baseUrl, credentials },
);
await use(page);
},
user: async ({ pageWithCredentials: page, credentials }, use) => {
await page.goto("/");
await page.waitForSelector(".mx_MatrixChat", { timeout: 30000 });
await use(credentials);
},
axe: async ({ page }, use) => {
await use(new AxeBuilder({ page }).exclude("[data-floating-ui-portal]"));
},
checkA11y: async ({ axe }, use, testInfo) =>
use(async () => {
const results = await axe.analyze();
await testInfo.attach("accessibility-scan-results", {
body: JSON.stringify(results, null, 2),
contentType: "application/json",
});
expect(results.violations).toEqual([]);
}),
app: async ({ page }, use) => {
const app = new ElementAppPage(page);
await use(app);
@@ -244,35 +103,23 @@ export const test = base.extend<TestFixtures>({
},
});
// Based on https://github.com/microsoft/playwright/blob/2b77ed4d7aafa85a600caa0b0d101b72c8437eeb/packages/playwright/src/util.ts#L206C8-L210C2
function sanitizeFilePathBeforeExtension(filePath: string): string {
const ext = extname(filePath);
const base = filePath.substring(0, filePath.length - ext.length);
return sanitizeForFilePath(base) + ext;
interface ExtendedToMatchScreenshotOptions extends ToMatchScreenshotOptions {
includeDialogBackground?: boolean;
showTooltips?: boolean;
timeout?: number;
}
export const expect = baseExpect.extend({
async toMatchScreenshot(
type Expectations = {
toMatchScreenshot: (
this: ExpectMatcherState,
receiver: Page | Locator,
name: `${string}.png`,
options?: {
mask?: Array<Locator>;
includeDialogBackground?: boolean;
showTooltips?: boolean;
timeout?: number;
css?: string;
},
) {
const testInfo = test.info();
if (!testInfo) throw new Error(`toMatchScreenshot() must be called during the test`);
if (!testInfo.tags.includes("@screenshot")) {
throw new Error("toMatchScreenshot() must be used in a test tagged with @screenshot");
}
const page = "page" in receiver ? receiver.page() : receiver;
options?: ExtendedToMatchScreenshotOptions,
) => Promise<MatcherReturnType>;
};
export const expect = baseExpect.extend<Expectations>({
async toMatchScreenshot(receiver, name, options) {
let css = `
.mx_MessagePanel_myReadMarker {
display: none !important;
@@ -322,21 +169,9 @@ export const expect = baseExpect.extend({
css += options.css;
}
// We add a custom style tag before taking screenshots
const style = (await page.addStyleTag({
content: css,
})) as ElementHandle<Element>;
const screenshotName = sanitizeFilePathBeforeExtension(name);
await baseExpect(receiver).toHaveScreenshot(screenshotName, options);
await style.evaluate((tag) => tag.remove());
testInfo.annotations.push({
// `_` prefix hides it from the HTML reporter
type: "_screenshot",
// include a path relative to `playwright/snapshots/`
description: testInfo.snapshotPath(screenshotName).split("/playwright/snapshots/", 2)[1],
await baseExpect(receiver).toMatchScreenshot(name, {
...options,
css,
});
return { pass: true, message: () => "", name: "toMatchScreenshot" };

View File

@@ -1,63 +0,0 @@
/*
Copyright 2024 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 BrowserContext, type Page, type TestInfo } from "@playwright/test";
import { type Readable } from "stream";
import stripAnsi from "strip-ansi";
export class Logger {
private pages: Page[] = [];
private logs: Record<string, string> = {};
public getConsumer(container: string) {
this.logs[container] = "";
return (stream: Readable) => {
stream.on("data", (chunk) => {
this.logs[container] += chunk.toString();
});
stream.on("err", (chunk) => {
this.logs[container] += "ERR " + chunk.toString();
});
};
}
public async onTestStarted(context: BrowserContext) {
this.pages = [];
for (const id in this.logs) {
if (id.startsWith("page-")) {
delete this.logs[id];
} else {
this.logs[id] = "";
}
}
context.on("console", (msg) => {
const page = msg.page();
let pageIdx = this.pages.indexOf(page);
if (pageIdx === -1) {
this.pages.push(page);
pageIdx = this.pages.length - 1;
this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`;
}
const type = msg.type();
const text = msg.text();
this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`;
});
}
public async onTestFinished(testInfo: TestInfo) {
if (testInfo.status !== "passed") {
for (const id in this.logs) {
if (!this.logs[id]) continue;
await testInfo.attach(id, {
body: stripAnsi(this.logs[id]),
contentType: "text/plain",
});
}
}
}
}

View File

@@ -6,8 +6,8 @@ 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 Options } from "../../../services.ts";
import { type WorkerOptions } from "../../../services.ts";
export const isDendrite = ({ homeserverType }: Options): boolean => {
export const isDendrite = ({ homeserverType }: WorkerOptions): boolean => {
return homeserverType === "dendrite" || homeserverType === "pinecone";
};

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
Please see LICENSE files in the repository root for full details.
*/
import { type ClientServerApi } from "../utils/api.ts";
import { type ClientServerApi } from "@element-hq/element-web-playwright-common/lib/utils/api.js";
export interface HomeserverInstance {
readonly baseUrl: string;

View File

@@ -6,30 +6,19 @@ 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 SynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
import { type Fixtures } from "../../../element-web-test.ts";
export const consentHomeserver: Fixtures = {
_homeserver: [
async ({ _homeserver: container, mailpit }, use) => {
container
(container as SynapseContainer)
.withCopyDirectoriesToContainer([
{ source: "playwright/plugins/homeserver/synapse/res", target: "/data/res" },
])
.withSmtpServer(mailpit)
.withConfig({
email: {
enable_notifs: false,
smtp_host: "mailpit",
smtp_port: 1025,
smtp_user: "username",
smtp_pass: "password",
require_transport_security: false,
notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
app_name: "Matrix",
notif_template_html: "notif_mail.html",
notif_template_text: "notif_mail.txt",
notif_for_new_users: true,
client_base_url: "http://localhost/element",
},
user_consent: {
template_dir: "/data/res/templates/privacy",
version: "1.0",

View File

@@ -6,7 +6,8 @@ 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 { MatrixAuthenticationServiceContainer } from "../../../testcontainers/mas.ts";
import { MatrixAuthenticationServiceContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
import { type Fixtures } from "../../../element-web-test.ts";
export const masHomeserver: Fixtures = {

View File

@@ -10,8 +10,7 @@ import http from "http";
import express from "express";
import { type AddressInfo } from "net";
import { type TestInfo } from "@playwright/test";
import { randB64Bytes } from "../utils/rand.ts";
import { randB64Bytes } from "@element-hq/element-web-playwright-common/lib/utils/rand.js";
export class OAuthServer {
private server?: http.Server;

View File

@@ -1,76 +0,0 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type APIRequestContext } from "@playwright/test";
import { type Credentials } from "../homeserver";
export type Verb = "GET" | "POST" | "PUT" | "DELETE";
export class Api {
private _request?: APIRequestContext;
constructor(private readonly baseUrl: string) {}
public setRequest(request: APIRequestContext): void {
this._request = request;
}
public async request<R extends {}>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
public async request<R extends {}>(verb: Verb, path: string, token?: string, data?: object): Promise<R> {
const url = `${this.baseUrl}${path}`;
const res = await this._request.fetch(url, {
data,
method: verb,
headers: token
? {
Authorization: `Bearer ${token}`,
}
: undefined,
});
if (!res.ok()) {
throw new Error(
`Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`,
);
}
return res.json();
}
}
export class ClientServerApi extends Api {
constructor(baseUrl: string) {
super(`${baseUrl}/_matrix/client`);
}
public async loginUser(userId: string, password: string): Promise<Credentials> {
const json = await this.request<{
access_token: string;
user_id: string;
device_id: string;
home_server: string;
}>("POST", "/v3/login", undefined, {
type: "m.login.password",
identifier: {
type: "m.id.user",
user: userId,
},
password: password,
});
return {
password,
accessToken: json.access_token,
userId: json.user_id,
deviceId: json.device_id,
homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"),
username: userId.slice(1).split(":")[0],
};
}
}

View File

@@ -1,16 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
/**
* Deep copy the given object. The object MUST NOT have circular references and
* MUST NOT have functions.
* @param obj - The object to deep copy.
* @returns A copy of the object without any references to the original.
*/
export function deepCopy<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}

View File

@@ -1,19 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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 * as net from "net";
export async function getFreePort(): Promise<number> {
return new Promise<number>((resolve) => {
const srv = net.createServer();
srv.listen(0, () => {
const port = (<net.AddressInfo>srv.address()).port;
srv.close(() => resolve(port));
});
});
}

View File

@@ -1,13 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
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 crypto from "node:crypto";
export function randB64Bytes(numBytes: number): string {
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
}

View File

@@ -6,8 +6,8 @@ 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 * as http from "http";
import { type AddressInfo } from "net";
import * as http from "node:http";
import { type AddressInfo } from "node:net";
export class Webserver {
private server?: http.Server;

View File

@@ -5,113 +5,32 @@ 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 { test as base } from "@playwright/test";
import { type MailpitClient } from "mailpit-api";
import { Network, type StartedNetwork } from "testcontainers";
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import { test as base } from "@element-hq/element-web-playwright-common";
import {
type Services as BaseServices,
type WorkerOptions as BaseWorkerOptions,
} from "@element-hq/element-web-playwright-common/lib/fixtures";
import { type HomeserverContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
import { type SynapseConfig, SynapseContainer } from "./testcontainers/synapse.ts";
import { Logger } from "./logger.ts";
import { type StartedMatrixAuthenticationServiceContainer } from "./testcontainers/mas.ts";
import { type HomeserverContainer, type StartedHomeserverContainer } from "./testcontainers/HomeserverContainer.ts";
import { MailhogContainer, type StartedMailhogContainer } from "./testcontainers/mailpit.ts";
import { type OAuthServer } from "./plugins/oauth_server";
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite.ts";
import { DendriteContainer, PineconeContainer } from "./testcontainers/dendrite";
import { type HomeserverType } from "./plugins/homeserver";
import { SynapseContainer } from "./testcontainers/synapse";
export interface TestFixtures {
mailpitClient: MailpitClient;
}
export interface Services {
logger: Logger;
network: StartedNetwork;
postgres: StartedPostgreSqlContainer;
mailpit: StartedMailhogContainer;
synapseConfig: SynapseConfig;
_homeserver: HomeserverContainer<any>;
homeserver: StartedHomeserverContainer;
// Set in masHomeserver only
mas?: StartedMatrixAuthenticationServiceContainer;
export interface Services extends BaseServices {
// Set in legacyOAuthHomeserver only
oAuthServer?: OAuthServer;
}
export interface Options {
export interface WorkerOptions extends BaseWorkerOptions {
homeserverType: HomeserverType;
}
export const test = base.extend<TestFixtures, Services & Options>({
logger: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
const logger = new Logger();
await use(logger);
},
{ scope: "worker" },
],
network: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
const network = await new Network().start();
await use(network);
await network.stop();
},
{ scope: "worker" },
],
postgres: [
async ({ logger, network }, use) => {
const container = await new PostgreSqlContainer()
.withNetwork(network)
.withNetworkAliases("postgres")
.withLogConsumer(logger.getConsumer("postgres"))
.withTmpFs({
"/dev/shm/pgdata/data": "",
})
.withEnvironment({
PG_DATA: "/dev/shm/pgdata/data",
})
.withCommand([
"-c",
"shared_buffers=128MB",
"-c",
`fsync=off`,
"-c",
`synchronous_commit=off`,
"-c",
"full_page_writes=off",
])
.start();
await use(container);
await container.stop();
},
{ scope: "worker" },
],
mailpit: [
async ({ logger, network }, use) => {
const container = await new MailhogContainer()
.withNetwork(network)
.withNetworkAliases("mailpit")
.withLogConsumer(logger.getConsumer("mailpit"))
.start();
await use(container);
await container.stop();
},
{ scope: "worker" },
],
mailpitClient: async ({ mailpit: container }, use) => {
await container.client.deleteMessages();
await use(container.client);
},
synapseConfig: [{}, { scope: "worker" }],
export const test = base.extend<{}, Services & WorkerOptions>({
homeserverType: ["synapse", { option: true, scope: "worker" }],
_homeserver: [
async ({ homeserverType }, use) => {
let container: HomeserverContainer<any>;
let container: HomeserverContainer<unknown>;
switch (homeserverType) {
case "synapse":
container = new SynapseContainer();
@@ -128,46 +47,12 @@ export const test = base.extend<TestFixtures, Services & Options>({
},
{ scope: "worker" },
],
homeserver: [
async ({ homeserverType, logger, network, _homeserver: homeserver, synapseConfig, mas }, use) => {
if (homeserver instanceof SynapseContainer) {
homeserver.withConfig(synapseConfig);
}
const container = await homeserver
.withNetwork(network)
.withNetworkAliases("homeserver")
.withLogConsumer(logger.getConsumer(homeserverType))
.withMatrixAuthenticationService(mas)
.start();
await use(container);
await container.stop();
},
{ scope: "worker" },
],
mas: [
// eslint-disable-next-line no-empty-pattern
async ({}, use) => {
// we stub the mas fixture to allow `homeserver` to depend on it to ensure
// when it is specified by `masHomeserver` it is started before the homeserver
await use(undefined);
},
{ scope: "worker" },
],
context: async (
{ homeserverType, synapseConfig, logger, context, request, _homeserver, homeserver },
use,
testInfo,
) => {
context: async ({ homeserverType, synapseConfig, context, _homeserver }, use, testInfo) => {
testInfo.skip(
!(_homeserver instanceof SynapseContainer) && Object.keys(synapseConfig).length > 0,
`Test specifies Synapse config options so is unsupported with ${homeserverType}`,
);
homeserver.setRequest(request);
await logger.onTestStarted(context);
await use(context);
await logger.onTestFinished(testInfo);
await homeserver.onTestFinished(testInfo);
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 227 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: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

After

Width:  |  Height:  |  Size: 985 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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