mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-13 01:50:46 +00:00
Compare commits
675 Commits
t3chguy/sp
...
v1.11.103
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb7359403f | ||
|
|
7fe53eac16 | ||
|
|
dba4952721 | ||
|
|
5cf543a9a7 | ||
|
|
713cd472c6 | ||
|
|
ee87b0e2d2 | ||
|
|
3fd52c9e07 | ||
|
|
e9c91ba28a | ||
|
|
45182172b8 | ||
|
|
8513eaa898 | ||
|
|
87447c7f91 | ||
|
|
f5125ac2b8 | ||
|
|
bd142412e5 | ||
|
|
581920e82b | ||
|
|
5d2d4947f4 | ||
|
|
69fe2ad06c | ||
|
|
ed0b50283e | ||
|
|
e7e425f3db | ||
|
|
f81a127d46 | ||
|
|
b539eda4fe | ||
|
|
22c7bf346c | ||
|
|
e1104891cb | ||
|
|
78ec757f11 | ||
|
|
45f41a33e7 | ||
|
|
b56b0f2bd0 | ||
|
|
4dcde7ec7a | ||
|
|
b07225eb60 | ||
|
|
9642af9930 | ||
|
|
c309cc8bfa | ||
|
|
fb65bbf521 | ||
|
|
57d3b2d93c | ||
|
|
aef3c8e986 | ||
|
|
1b48269db5 | ||
|
|
76d7f6ab43 | ||
|
|
69c1a8cd1c | ||
|
|
231515bc6c | ||
|
|
ccd77be74a | ||
|
|
be5dd058b3 | ||
|
|
2326a7c8dc | ||
|
|
c52ec3efd1 | ||
|
|
785a12a029 | ||
|
|
c9548ec1d0 | ||
|
|
fadd54f0b3 | ||
|
|
6ac66da5eb | ||
|
|
8be05f0ad9 | ||
|
|
5faae73055 | ||
|
|
32dfabbcb6 | ||
|
|
972366b5ae | ||
|
|
597a0d25ac | ||
|
|
fe8d5aee63 | ||
|
|
900fa53a33 | ||
|
|
706d929f3a | ||
|
|
2b5f687c40 | ||
|
|
501c8194e5 | ||
|
|
138c40b0c1 | ||
|
|
85647efadb | ||
|
|
16d57074df | ||
|
|
c84cf3c36c | ||
|
|
e235100dd0 | ||
|
|
10757b4357 | ||
|
|
64047b0702 | ||
|
|
0d5a8aafbd | ||
|
|
fb5c4ffc8b | ||
|
|
308f892cef | ||
|
|
54a00baff8 | ||
|
|
08acbf9b14 | ||
|
|
a3f5d207de | ||
|
|
0f783ede5e | ||
|
|
e427b71040 | ||
|
|
d553be6316 | ||
|
|
6063209fff | ||
|
|
36d25da288 | ||
|
|
74fbd892a1 | ||
|
|
6ba21dafa7 | ||
|
|
8ac2f60720 | ||
|
|
64f0dfe0bc | ||
|
|
a728385385 | ||
|
|
d9926c8784 | ||
|
|
186f7e71be | ||
|
|
9eb90a8204 | ||
|
|
9ec54c534d | ||
|
|
671e55c5a2 | ||
|
|
a430501271 | ||
|
|
72429c1350 | ||
|
|
8e63c6618c | ||
|
|
f25fbdebc7 | ||
|
|
ce1055f5fe | ||
|
|
4bf28f8159 | ||
|
|
23597e959b | ||
|
|
4f4f391959 | ||
|
|
cd05838bf6 | ||
|
|
290643934d | ||
|
|
6a18cca76b | ||
|
|
421a79aaa5 | ||
|
|
269f01fee1 | ||
|
|
8864ba939f | ||
|
|
67be444c75 | ||
|
|
57d39a8d34 | ||
|
|
3800584f5a | ||
|
|
290916a310 | ||
|
|
9f560f1f89 | ||
|
|
8e3fb5288b | ||
|
|
02dd79f03f | ||
|
|
160a7c1ae3 | ||
|
|
c3c04323e1 | ||
|
|
d6a1d9aa3d | ||
|
|
a8ca4ff90c | ||
|
|
83e6753c4e | ||
|
|
adc110a8d9 | ||
|
|
6329f69557 | ||
|
|
ce4b9860a8 | ||
|
|
714f8f40dd | ||
|
|
22d5c00174 | ||
|
|
5e7b58a722 | ||
|
|
40debba4dd | ||
|
|
ee59849307 | ||
|
|
5933f50930 | ||
|
|
f6a3a429f7 | ||
|
|
5dba03dff2 | ||
|
|
8269770db0 | ||
|
|
d0c69b4e35 | ||
|
|
75d9898dff | ||
|
|
da6ac36f11 | ||
|
|
c1f145d802 | ||
|
|
81260fef57 | ||
|
|
09ceb3c580 | ||
|
|
1077729a19 | ||
|
|
4f32727829 | ||
|
|
fd455179f7 | ||
|
|
6767e4d6ad | ||
|
|
1743257ca0 | ||
|
|
d2fdd45c47 | ||
|
|
f250575b08 | ||
|
|
db9428de87 | ||
|
|
b511bf064d | ||
|
|
427e61309b | ||
|
|
aa821a5b6f | ||
|
|
8b06714a02 | ||
|
|
079e1fcbc8 | ||
|
|
07c1b406ac | ||
|
|
af9bde5137 | ||
|
|
ee8c1ffef4 | ||
|
|
fa1043426a | ||
|
|
18a7250cf9 | ||
|
|
7e5f96c85d | ||
|
|
a8e0b54d8a | ||
|
|
302e3e153e | ||
|
|
7642054b74 | ||
|
|
f6955124ac | ||
|
|
9207f25dc3 | ||
|
|
f476da8bec | ||
|
|
34e08af274 | ||
|
|
6c4bd0c8b1 | ||
|
|
88e06cdc55 | ||
|
|
55c4b2fac0 | ||
|
|
84479a86f3 | ||
|
|
8e3830acee | ||
|
|
6fc3dd4628 | ||
|
|
c313c720de | ||
|
|
23a42e0d54 | ||
|
|
bb23a98bc6 | ||
|
|
d52b0a1467 | ||
|
|
986be9c00d | ||
|
|
475e449e81 | ||
|
|
7ce0a76414 | ||
|
|
2e71ec748f | ||
|
|
07d5a72f26 | ||
|
|
1430fd5af6 | ||
|
|
779543fa0f | ||
|
|
6b052fd067 | ||
|
|
f39f3d2164 | ||
|
|
c929eedd81 | ||
|
|
bcd396e19e | ||
|
|
ca56c2e091 | ||
|
|
d594441b53 | ||
|
|
d4f25e8e13 | ||
|
|
d70d4486f0 | ||
|
|
60117b92d8 | ||
|
|
afc8536d1c | ||
|
|
b5993aaabb | ||
|
|
e1b2e3a101 | ||
|
|
f54fbf7231 | ||
|
|
01bfaec729 | ||
|
|
ab51ff6b7e | ||
|
|
803cb36d60 | ||
|
|
24167871e6 | ||
|
|
1fdd313ae9 | ||
|
|
18cd641cf6 | ||
|
|
2bc7223c1c | ||
|
|
8fc6638d6e | ||
|
|
e2b7852998 | ||
|
|
c24a1baf38 | ||
|
|
d337106eed | ||
|
|
5ce5e9092b | ||
|
|
cb657d6848 | ||
|
|
1f9db9fa1a | ||
|
|
ac3667508f | ||
|
|
149b3b1049 | ||
|
|
d07a02fe3d | ||
|
|
9d8d407019 | ||
|
|
617fcdd4ce | ||
|
|
df38e16dbb | ||
|
|
817d7b78b8 | ||
|
|
31a59a5fa3 | ||
|
|
55f1c27184 | ||
|
|
92b85fcb13 | ||
|
|
82d93695a2 | ||
|
|
637ba3222e | ||
|
|
abbc1c0947 | ||
|
|
602e65ff52 | ||
|
|
e915e40e39 | ||
|
|
35bf6afe55 | ||
|
|
52c8867e67 | ||
|
|
b217271027 | ||
|
|
286231aa37 | ||
|
|
3f20df5e08 | ||
|
|
d5e070b300 | ||
|
|
d8ecb6362a | ||
|
|
bcc4ecf0cb | ||
|
|
24d9a174d7 | ||
|
|
7970b968c2 | ||
|
|
59e591c462 | ||
|
|
804cb62698 | ||
|
|
8bb4d44532 | ||
|
|
209ab59978 | ||
|
|
6ae11dab52 | ||
|
|
fac982811c | ||
|
|
d7730f417b | ||
|
|
829b588dbf | ||
|
|
9a7cc7eb34 | ||
|
|
e537da4251 | ||
|
|
094a7071e2 | ||
|
|
a5673f603f | ||
|
|
0c210b9b3a | ||
|
|
05df321f34 | ||
|
|
8116dc5f60 | ||
|
|
d090499329 | ||
|
|
6784d071a6 | ||
|
|
3f47487472 | ||
|
|
89e22e00fb | ||
|
|
bbd798ef36 | ||
|
|
f3f05874fa | ||
|
|
d9091bcba9 | ||
|
|
68692c5af5 | ||
|
|
03dc093e89 | ||
|
|
c68157ec46 | ||
|
|
4fc8b8915b | ||
|
|
690d623dcf | ||
|
|
102a1ddb9e | ||
|
|
99ea51c6f2 | ||
|
|
3f1e56b715 | ||
|
|
f3653abe92 | ||
|
|
a6e8d512d0 | ||
|
|
13c4ab2cf4 | ||
|
|
74da64db63 | ||
|
|
e5d37a324d | ||
|
|
d0c1610bd2 | ||
|
|
64e2a843c3 | ||
|
|
fba59381a0 | ||
|
|
e1970df704 | ||
|
|
b54122884c | ||
|
|
0d28df0f67 | ||
|
|
3a39486468 | ||
|
|
0dc295e3b8 | ||
|
|
5a6c9a4c9a | ||
|
|
599112e122 | ||
|
|
170dcd1c0e | ||
|
|
435d0f96b8 | ||
|
|
c1a44414ec | ||
|
|
a32704ae5b | ||
|
|
5b1be70ee8 | ||
|
|
a6ae04bcde | ||
|
|
b65d18433d | ||
|
|
3587161a2c | ||
|
|
35aed69604 | ||
|
|
d2c334dd25 | ||
|
|
98470b8045 | ||
|
|
4d97af0baf | ||
|
|
f59af3786e | ||
|
|
4fa540962a | ||
|
|
e4f9c650ee | ||
|
|
f3654e45d6 | ||
|
|
2a8b26d90a | ||
|
|
6ed811d4c9 | ||
|
|
c85e6d196d | ||
|
|
98c691670e | ||
|
|
7e3866dd9a | ||
|
|
c6b1a92f2e | ||
|
|
7b809171fc | ||
|
|
0bef212679 | ||
|
|
56d115c2ff | ||
|
|
cdd2622151 | ||
|
|
e662c1959b | ||
|
|
b5f8f2b9f5 | ||
|
|
425adb147a | ||
|
|
839329b52a | ||
|
|
7de54a385e | ||
|
|
55b0b1107e | ||
|
|
550f529a30 | ||
|
|
a6ad6e9ae2 | ||
|
|
d88776e2dc | ||
|
|
ff1da50dd9 | ||
|
|
4af5d4ac80 | ||
|
|
a858fed321 | ||
|
|
f3dbe81ef4 | ||
|
|
ceba762caf | ||
|
|
9fb52e984c | ||
|
|
c31f5521ec | ||
|
|
66d9d717c4 | ||
|
|
4e3daa5df5 | ||
|
|
f9a0bb2904 | ||
|
|
f4b03a1b06 | ||
|
|
be3778bef0 | ||
|
|
973d639d01 | ||
|
|
20d8abf7c2 | ||
|
|
fda658182a | ||
|
|
9bfea92b66 | ||
|
|
962136d453 | ||
|
|
917d53a56f | ||
|
|
e44ca88a7e | ||
|
|
cb7d77de45 | ||
|
|
cd6737942f | ||
|
|
a058d85c21 | ||
|
|
bf6ae73d39 | ||
|
|
273cdf41e9 | ||
|
|
3ab3041c45 | ||
|
|
2052080d7d | ||
|
|
cc95d154fb | ||
|
|
4e696d2dc6 | ||
|
|
610b14adc1 | ||
|
|
324dd5a858 | ||
|
|
bc4bc6c25e | ||
|
|
3f3fba99eb | ||
|
|
26a17f9314 | ||
|
|
fd91e78152 | ||
|
|
af476905b6 | ||
|
|
da87bbe854 | ||
|
|
47976447b5 | ||
|
|
53065f9437 | ||
|
|
179b368809 | ||
|
|
82957507d0 | ||
|
|
27c1b38dab | ||
|
|
7ff1fd259d | ||
|
|
8d891cde53 | ||
|
|
90cc44b340 | ||
|
|
b6c872142b | ||
|
|
3762d40620 | ||
|
|
42192cbe06 | ||
|
|
aa996010b4 | ||
|
|
e9a3625bd6 | ||
|
|
343dd06929 | ||
|
|
77b6c3b526 | ||
|
|
b721505211 | ||
|
|
56c7fc1948 | ||
|
|
9d8efacede | ||
|
|
1770b94ed3 | ||
|
|
dfdac8ef63 | ||
|
|
f1ebd85af1 | ||
|
|
4776a9971d | ||
|
|
20ac69f379 | ||
|
|
8c42b0bed8 | ||
|
|
fbc6f12408 | ||
|
|
b82c8554e3 | ||
|
|
3d705b1895 | ||
|
|
81c12db5ee | ||
|
|
e1d76e77a5 | ||
|
|
54e015706c | ||
|
|
cef25c2cab | ||
|
|
59c26fc3ad | ||
|
|
31af8b07dd | ||
|
|
c0d14daa17 | ||
|
|
ed35a7cba4 | ||
|
|
21e9d93e69 | ||
|
|
ffa8971195 | ||
|
|
072ee0cf36 | ||
|
|
4b02520453 | ||
|
|
bf48100d31 | ||
|
|
2da21248bb | ||
|
|
3c57323595 | ||
|
|
4c72f0c0b2 | ||
|
|
dfd08a8c01 | ||
|
|
7db909a47d | ||
|
|
1ad1387e05 | ||
|
|
c6b3bf962a | ||
|
|
e749b017c9 | ||
|
|
c00262f0c5 | ||
|
|
7a513a2dc2 | ||
|
|
808412c6be | ||
|
|
45497905be | ||
|
|
0997e0a747 | ||
|
|
6173c1224b | ||
|
|
f95218e2b7 | ||
|
|
62a287219d | ||
|
|
db45a17d43 | ||
|
|
9b1de5634d | ||
|
|
230e26e1ab | ||
|
|
cf5ffacff2 | ||
|
|
fe353542cb | ||
|
|
0b624bf645 | ||
|
|
3cc1ccd029 | ||
|
|
05b8fff58a | ||
|
|
43efd911c7 | ||
|
|
2e883b40eb | ||
|
|
14e3a77dc2 | ||
|
|
efc6149a8b | ||
|
|
8ef84349b5 | ||
|
|
7d94fa9b03 | ||
|
|
37136ecf46 | ||
|
|
99b9eee86e | ||
|
|
27c0e97e44 | ||
|
|
0cbc6f99d0 | ||
|
|
d8904a6e56 | ||
|
|
22943ee06a | ||
|
|
2e1798edc4 | ||
|
|
3470182410 | ||
|
|
9d8d38eeb8 | ||
|
|
fb57924350 | ||
|
|
e8954f08ce | ||
|
|
e161f9fb18 | ||
|
|
e47d7aaaff | ||
|
|
8857c07acb | ||
|
|
28ed506fe1 | ||
|
|
76b3be6263 | ||
|
|
6c768b8b32 | ||
|
|
809ada17a4 | ||
|
|
c7762a80f1 | ||
|
|
261923832d | ||
|
|
3daa1bf06a | ||
|
|
e5c8d7dbf0 | ||
|
|
441119ca3a | ||
|
|
acb3e781a4 | ||
|
|
3fb1f6ef4d | ||
|
|
cbfbfad959 | ||
|
|
7546bbc1f0 | ||
|
|
6a10c86d7a | ||
|
|
6f9e3bfe3e | ||
|
|
61d55462df | ||
|
|
d391c69e53 | ||
|
|
a6afff9759 | ||
|
|
073d8e0b86 | ||
|
|
ecf5d720b0 | ||
|
|
8c4996b437 | ||
|
|
03d27e2808 | ||
|
|
b7fea97bb6 | ||
|
|
90801eb38b | ||
|
|
e02da752f0 | ||
|
|
dd2da5c132 | ||
|
|
52060235e4 | ||
|
|
8d67e88b1d | ||
|
|
a365533367 | ||
|
|
6dbc3b489a | ||
|
|
f822653d65 | ||
|
|
09db599fe0 | ||
|
|
c47ce59478 | ||
|
|
f9a85d37fa | ||
|
|
2abd5342c2 | ||
|
|
85f80b1d0a | ||
|
|
4b9382f888 | ||
|
|
03a5ee1c5b | ||
|
|
902146a829 | ||
|
|
43e918b71e | ||
|
|
53c97dfa50 | ||
|
|
f7b010a0b3 | ||
|
|
161323b595 | ||
|
|
8bf3ec29b9 | ||
|
|
039b95eba0 | ||
|
|
6b80d3fca0 | ||
|
|
784abbbe14 | ||
|
|
4d55e8f433 | ||
|
|
02990bd275 | ||
|
|
6fa8032caa | ||
|
|
67658aef56 | ||
|
|
f2fae82e32 | ||
|
|
ef69c0ddc7 | ||
|
|
bc7fe25974 | ||
|
|
426a2066d9 | ||
|
|
047e8e8a9c | ||
|
|
4de9fe60ae | ||
|
|
52b42c0b1c | ||
|
|
bb8b4d7991 | ||
|
|
ec994884fb | ||
|
|
4a8ba810a9 | ||
|
|
40a6c69f1a | ||
|
|
eed6b6df12 | ||
|
|
7b3ce5d9b2 | ||
|
|
8941724020 | ||
|
|
4a231c6450 | ||
|
|
3c690e685a | ||
|
|
53f83124a0 | ||
|
|
63a3a6c454 | ||
|
|
0358b7f93c | ||
|
|
4a381c2a10 | ||
|
|
7cafa0d1a4 | ||
|
|
9ae4388bef | ||
|
|
511c7ca6ab | ||
|
|
2dca721ae7 | ||
|
|
272bb6c5a2 | ||
|
|
fc0797a98d | ||
|
|
5a2edba21b | ||
|
|
9657d39cd6 | ||
|
|
1c4e35606c | ||
|
|
23a3bcfc73 | ||
|
|
25fba1f8ec | ||
|
|
409c0869ce | ||
|
|
c1cc6ab391 | ||
|
|
b4832fd936 | ||
|
|
6b3ae95e8b | ||
|
|
10e91b6e63 | ||
|
|
56083777ef | ||
|
|
70df19406e | ||
|
|
2673085afa | ||
|
|
d9001d177c | ||
|
|
7eb969bbc2 | ||
|
|
0a8393c9e1 | ||
|
|
0fa52e610e | ||
|
|
8cae1e9f5e | ||
|
|
1ea1d386ab | ||
|
|
afa6f377ea | ||
|
|
b7f8623617 | ||
|
|
e75ba356d3 | ||
|
|
4f1eac67a8 | ||
|
|
aa01b17f9e | ||
|
|
4cba79ddcc | ||
|
|
b64471e4f6 | ||
|
|
d3a6f34881 | ||
|
|
dcce9c70dc | ||
|
|
f06ed2fa1f | ||
|
|
099c3073b6 | ||
|
|
12932e2dc6 | ||
|
|
a7de29429c | ||
|
|
d3ea250d77 | ||
|
|
f243fee5a6 | ||
|
|
296d0074ed | ||
|
|
df83338f26 | ||
|
|
c0336f21f6 | ||
|
|
d88f47bdbc | ||
|
|
4a26414957 | ||
|
|
886d0e1241 | ||
|
|
c453d33456 | ||
|
|
ddf221b813 | ||
|
|
08238bb883 | ||
|
|
c390ec333e | ||
|
|
3c22e5dc68 | ||
|
|
f29ce94dd4 | ||
|
|
76485cfb17 | ||
|
|
c0567fc5f4 | ||
|
|
95da3834f2 | ||
|
|
b8a3468485 | ||
|
|
b7f6e0f88c | ||
|
|
b36d9ce32e | ||
|
|
9b6be0f5a9 | ||
|
|
cae0da8f00 | ||
|
|
25689de34a | ||
|
|
ce6cb47943 | ||
|
|
850c1a5b3a | ||
|
|
ec4ae9e58a | ||
|
|
a73eb378d7 | ||
|
|
197afd6a9e | ||
|
|
ac565dca80 | ||
|
|
a0044d6b5f | ||
|
|
68c03db557 | ||
|
|
9a109cdce8 | ||
|
|
a0ab88943b | ||
|
|
ad01218942 | ||
|
|
e1e4d26154 | ||
|
|
84c614676d | ||
|
|
29d9e98111 | ||
|
|
9f5f898ed8 | ||
|
|
78251a3a8a | ||
|
|
1b077c53f5 | ||
|
|
68828a2326 | ||
|
|
af8d93f58a | ||
|
|
c0a097867e | ||
|
|
0b13e57518 | ||
|
|
8615b411b2 | ||
|
|
3d31376b1d | ||
|
|
43e5124cd4 | ||
|
|
19674cca08 | ||
|
|
6ca6cb0fbe | ||
|
|
d92fc5a595 | ||
|
|
b9d411eecc | ||
|
|
3da6619bcf | ||
|
|
f33e7c9782 | ||
|
|
1ebae09834 | ||
|
|
790a976421 | ||
|
|
1e1d66924f | ||
|
|
63ecb48d7d | ||
|
|
5e3fc8aa19 | ||
|
|
56eafc908e | ||
|
|
1644169ff3 | ||
|
|
cf895b4296 | ||
|
|
e9d4f39e9d | ||
|
|
7c0ec21365 | ||
|
|
72df9c9076 | ||
|
|
e42ee727b4 | ||
|
|
7d30413178 | ||
|
|
8884e77ce3 | ||
|
|
58f812ffe6 | ||
|
|
ef1597ff2d | ||
|
|
e5ca7954c8 | ||
|
|
13913ba8b2 | ||
|
|
03a1b48e1f | ||
|
|
ef3bf59656 | ||
|
|
7a3202b537 | ||
|
|
dbce48b23d | ||
|
|
bb41616d5f | ||
|
|
c75f6dc3a1 | ||
|
|
880048d998 | ||
|
|
24685dc7d1 | ||
|
|
60f70b93e0 | ||
|
|
2559cba482 | ||
|
|
5882b004f5 | ||
|
|
37f8d70d89 | ||
|
|
e2bd040c88 | ||
|
|
381b2ea343 | ||
|
|
41944e5c6e | ||
|
|
540580504d | ||
|
|
1a21b718d8 | ||
|
|
2cddb16a9f | ||
|
|
671d6de805 | ||
|
|
0f8a2e93ce | ||
|
|
bff2d680e6 | ||
|
|
5a5db19c2c | ||
|
|
11a8723c73 | ||
|
|
e14a3b64c3 | ||
|
|
f99d7ce2bb | ||
|
|
585aa75525 | ||
|
|
effef7eaa7 | ||
|
|
9826a8851d | ||
|
|
ebef0d353e | ||
|
|
f1899b9eb1 | ||
|
|
027891a35a | ||
|
|
2f7c28ded0 | ||
|
|
b6aba1477b | ||
|
|
056ecbb138 | ||
|
|
7685e547de | ||
|
|
a0a4211447 | ||
|
|
0ad4e13e2d | ||
|
|
f406305510 | ||
|
|
6cb174e3d9 | ||
|
|
c569478240 | ||
|
|
2bd8e049c7 | ||
|
|
e8d69dc592 | ||
|
|
50ac509a01 | ||
|
|
3e27a0019d | ||
|
|
5caad70191 | ||
|
|
6846679d34 | ||
|
|
7e5420100a | ||
|
|
f75d1f5a5e | ||
|
|
66bbb84e56 | ||
|
|
48152d2cd1 | ||
|
|
0d50e34763 | ||
|
|
f157c90ba9 | ||
|
|
cccb847d4e | ||
|
|
a5b739c45a | ||
|
|
9b1e165e6c | ||
|
|
0e2b16abf1 | ||
|
|
f6e999c87d | ||
|
|
3983bd5646 | ||
|
|
e8402f1657 | ||
|
|
69237e7df2 | ||
|
|
cf1c0805f1 | ||
|
|
b6a1aea825 | ||
|
|
b97005c182 | ||
|
|
9599c57a20 | ||
|
|
3eb3b936d9 | ||
|
|
b488155910 | ||
|
|
afc0dd5f86 | ||
|
|
7acadc29cc | ||
|
|
a73faffe37 | ||
|
|
1e758cacae | ||
|
|
7b565e7997 | ||
|
|
b16088d098 | ||
|
|
29624f7bcb | ||
|
|
d8f6c12c3d |
@@ -7,3 +7,7 @@ test/end-to-end-tests/lib/
|
|||||||
src/component-index.js
|
src/component-index.js
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
src/modules.ts
|
src/modules.ts
|
||||||
|
src/modules.js
|
||||||
|
# Test result files
|
||||||
|
/playwright/test-results/
|
||||||
|
/playwright/html-report/
|
||||||
|
|||||||
23
.eslintrc.js
23
.eslintrc.js
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org"],
|
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||||
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
@@ -30,6 +30,10 @@ module.exports = {
|
|||||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
"Use UIStore to access window dimensions instead.",
|
"Use UIStore to access window dimensions instead.",
|
||||||
),
|
),
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["React.forwardRef", "*.forwardRef", "forwardRef"],
|
||||||
|
"Use ref props instead.",
|
||||||
|
),
|
||||||
...buildRestrictedPropertiesOptions(
|
...buildRestrictedPropertiesOptions(
|
||||||
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
||||||
"Use Media helper instead to centralise access for customisation.",
|
"Use Media helper instead to centralise access for customisation.",
|
||||||
@@ -55,6 +59,11 @@ module.exports = {
|
|||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
paths: [
|
paths: [
|
||||||
|
{
|
||||||
|
name: "react",
|
||||||
|
importNames: ["forwardRef"],
|
||||||
|
message: "Use ref props instead.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "@testing-library/react",
|
name: "@testing-library/react",
|
||||||
message: "Please use jest-matrix-react instead",
|
message: "Please use jest-matrix-react instead",
|
||||||
@@ -170,6 +179,8 @@ module.exports = {
|
|||||||
"jsx-a11y/role-supports-aria-props": "off",
|
"jsx-a11y/role-supports-aria-props": "off",
|
||||||
|
|
||||||
"matrix-org/require-copyright-header": "error",
|
"matrix-org/require-copyright-header": "error",
|
||||||
|
|
||||||
|
"react-compiler/react-compiler": "error",
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
@@ -198,8 +209,13 @@ module.exports = {
|
|||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
// We're okay with assertion errors when we ask for them
|
// We're okay with assertion errors when we ask for them
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-empty-object-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
// We do this sometimes to brand interfaces
|
// We do this sometimes to brand interfaces
|
||||||
"@typescript-eslint/no-empty-object-type": "off",
|
allowInterfaces: "with-single-extends",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// temporary override for offending icon require files
|
// temporary override for offending icon require files
|
||||||
@@ -245,6 +261,7 @@ module.exports = {
|
|||||||
// We don't need super strict typing in test utilities
|
// We don't need super strict typing in test utilities
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
"@typescript-eslint/explicit-member-accessibility": "off",
|
"@typescript-eslint/explicit-member-accessibility": "off",
|
||||||
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
|
|
||||||
// Jest/Playwright specific
|
// Jest/Playwright specific
|
||||||
|
|
||||||
@@ -262,6 +279,7 @@ module.exports = {
|
|||||||
|
|
||||||
// These are fine in tests
|
// These are fine in tests
|
||||||
"no-restricted-globals": "off",
|
"no-restricted-globals": "off",
|
||||||
|
"react-compiler/react-compiler": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -271,6 +289,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react-hooks/rules-of-hooks": ["off"],
|
"react-hooks/rules-of-hooks": ["off"],
|
||||||
|
"@typescript-eslint/no-floating-promises": ["error"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
8
.github/CODEOWNERS
vendored
8
.github/CODEOWNERS
vendored
@@ -10,10 +10,16 @@
|
|||||||
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
/test/components/views/dialogs/security/ @element-hq/element-crypto-web-reviewers
|
||||||
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
/src/stores/SetupEncryptionStore.ts @element-hq/element-crypto-web-reviewers
|
||||||
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
/test/stores/SetupEncryptionStore-test.ts @element-hq/element-crypto-web-reviewers
|
||||||
|
/src/components/views/settings/tabs/user/EncryptionUserSettingsTab.tsx @element-hq/element-crypto-web-reviewers
|
||||||
|
/src/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/test/unit-tests/components/views/settings/encryption/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/src/components/views/dialogs/devtools/Crypto.tsx @element-hq/element-crypto-web-reviewers
|
||||||
|
/playwright/e2e/crypto/ @element-hq/element-crypto-web-reviewers
|
||||||
|
/playwright/e2e/settings/encryption-user-tab/ @element-hq/element-crypto-web-reviewers
|
||||||
|
|
||||||
# Ignore translations as those will be updated by GHA for Localazy download
|
# Ignore translations as those will be updated by GHA for Localazy download
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
/src/i18n/strings/en_EN.json @element-hq/element-web-reviewers
|
||||||
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
# Ignore the synapse plugin as this is updated by GHA for docker image updating
|
||||||
/playwright/plugins/homeserver/synapse/index.ts
|
/playwright/testcontainers/synapse.ts
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: Download release tarball
|
- name: Download release tarball
|
||||||
uses: robinraju/release-downloader@a96f54c1b5f5e09e47d9504526e96febd949d4c2 # v1
|
uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1
|
||||||
with:
|
with:
|
||||||
tag: ${{ inputs.tag }}
|
tag: ${{ inputs.tag }}
|
||||||
fileName: element-*.tar.gz*
|
fileName: element-*.tar.gz*
|
||||||
|
|||||||
12
.github/labels.yml
vendored
12
.github/labels.yml
vendored
@@ -210,6 +210,9 @@
|
|||||||
- name: "X-Upcoming-Release-Blocker"
|
- name: "X-Upcoming-Release-Blocker"
|
||||||
description: "This does not affect the current release cycle but will affect the next one"
|
description: "This does not affect the current release cycle but will affect the next one"
|
||||||
color: "e99695"
|
color: "e99695"
|
||||||
|
- name: "X-Run-All-Tests"
|
||||||
|
description: "When applied to PRs, it'll run the full gamut of end-to-end tests on the PR"
|
||||||
|
color: "ff7979"
|
||||||
- name: "Z-Actions"
|
- name: "Z-Actions"
|
||||||
color: "ededed"
|
color: "ededed"
|
||||||
- name: "Z-Cache-Confusion"
|
- name: "Z-Cache-Confusion"
|
||||||
@@ -232,6 +235,15 @@
|
|||||||
- name: "Z-Flaky-Test"
|
- name: "Z-Flaky-Test"
|
||||||
description: "A test is raising false alarms"
|
description: "A test is raising false alarms"
|
||||||
color: "ededed"
|
color: "ededed"
|
||||||
|
- name: "Z-Flaky-Test-Chrome"
|
||||||
|
description: "Flaky playwright test in Chrome"
|
||||||
|
color: "ededed"
|
||||||
|
- name: "Z-Flaky-Test-Firefox"
|
||||||
|
description: "Flaky playwright test in Firefox"
|
||||||
|
color: "ededed"
|
||||||
|
- name: "Z-Flaky-Test-Webkit"
|
||||||
|
description: "Flaky playwright test in Webkit"
|
||||||
|
color: "ededed"
|
||||||
- name: "Z-Flaky-Jest-Test"
|
- name: "Z-Flaky-Jest-Test"
|
||||||
description: "A Jest test is raising false alarms"
|
description: "A Jest test is raising false alarms"
|
||||||
color: "ededed"
|
color: "ededed"
|
||||||
|
|||||||
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
@@ -27,18 +27,25 @@ jobs:
|
|||||||
- macos-14
|
- macos-14
|
||||||
isDevelop:
|
isDevelop:
|
||||||
- ${{ github.event_name == 'push' && github.ref_name == 'develop' }}
|
- ${{ github.event_name == 'push' && github.ref_name == 'develop' }}
|
||||||
|
isPullRequest:
|
||||||
|
- ${{ github.event_name == 'pull_request' }}
|
||||||
# Skip the ubuntu-24.04 build for the develop branch as the dedicated CD build_develop workflow handles that
|
# Skip the ubuntu-24.04 build for the develop branch as the dedicated CD build_develop workflow handles that
|
||||||
|
# Skip the non-linux builds for pull requests as Windows is awfully slow, so run in merge queue only
|
||||||
exclude:
|
exclude:
|
||||||
- isDevelop: true
|
- isDevelop: true
|
||||||
image: ubuntu-24.04
|
image: ubuntu-24.04
|
||||||
|
- isPullRequest: true
|
||||||
|
image: windows-2022
|
||||||
|
- isPullRequest: true
|
||||||
|
image: macos-14
|
||||||
runs-on: ${{ matrix.image }}
|
runs-on: ${{ matrix.image }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
shell: bash
|
shell: bash
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
# Disable cache on Windows as it is slower than not caching
|
# Disable cache on Windows as it is slower than not caching
|
||||||
# https://github.com/actions/setup-node/issues/975
|
# https://github.com/actions/setup-node/issues/975
|
||||||
@@ -70,7 +77,7 @@ jobs:
|
|||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp-${{ matrix.image }}
|
name: webapp-${{ matrix.image }}
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|||||||
4
.github/workflows/build_debian.yaml
vendored
4
.github/workflows/build_debian.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
VERSION: ${{ github.ref_name }}
|
VERSION: ${{ github.ref_name }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Download package
|
- name: Download package
|
||||||
run: |
|
run: |
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
|
dpkg-gencontrol -v"$VERSION" -ldebian/tmp/DEBIAN/changelog
|
||||||
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
|
dpkg-deb -Zxz --root-owner-group --build debian/tmp element-web.deb
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: element-web.deb
|
name: element-web.deb
|
||||||
path: element-web.deb
|
path: element-web.deb
|
||||||
|
|||||||
11
.github/workflows/build_develop.yml
vendored
11
.github/workflows/build_develop.yml
vendored
@@ -26,9 +26,9 @@ jobs:
|
|||||||
R2_URL: ${{ vars.CF_R2_S3_API }}
|
R2_URL: ${{ vars.CF_R2_S3_API }}
|
||||||
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
R2_PUBLIC_URL: "https://element-web-develop.element.io"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
|
- run: mv dist/element-*.tar.gz dist/develop.tar.gz
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: dist/develop.tar.gz
|
path: dist/develop.tar.gz
|
||||||
@@ -109,10 +109,11 @@ jobs:
|
|||||||
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
# We keep the latest develop.tar.gz on R2 instead of relying on the github artifact uploaded earlier
|
||||||
# as the expires after 24h and requires auth to download.
|
# as the expires after 24h and requires auth to download.
|
||||||
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
# Element Desktop's fetch script uses this tarball to fetch latest develop to build Nightlies.
|
||||||
|
# Checksum algorithm specified as per https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||||
- name: Deploy to R2
|
- name: Deploy to R2
|
||||||
run: |
|
run: |
|
||||||
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto
|
aws s3 cp dist/develop.tar.gz s3://$R2_BUCKET/develop.tar.gz --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||||
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto
|
aws s3 cp _deploy/ s3://$R2_BUCKET/ --recursive --endpoint-url $R2_URL --region=auto --checksum-algorithm CRC32
|
||||||
env:
|
env:
|
||||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_TOKEN }}
|
||||||
|
|||||||
3
.github/workflows/deploy.yml
vendored
3
.github/workflows/deploy.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SITE: ${{ inputs.site || 'staging.element.io' }}
|
SITE: ${{ inputs.site || 'staging.element.io' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Load GPG key
|
- name: Load GPG key
|
||||||
run: |
|
run: |
|
||||||
@@ -96,3 +96,4 @@ jobs:
|
|||||||
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
projectName: ${{ env.SITE == 'staging.element.io' && 'element-web-staging' || 'element-web' }}
|
||||||
directory: _deploy
|
directory: _deploy
|
||||||
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
gitHubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
branch: main
|
||||||
|
|||||||
141
.github/workflows/docker.yaml
vendored
Normal file
141
.github/workflows/docker.yaml
vendored
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
name: Docker
|
||||||
|
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 * * *"
|
||||||
|
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
buildx:
|
||||||
|
name: Docker Buildx
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed for docker-package to be able to calculate the version
|
||||||
|
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
|
with:
|
||||||
|
install: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
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@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
vectorim/element-web
|
||||||
|
ghcr.io/element-hq/element-web
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
flavor: |
|
||||||
|
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build-and-push
|
||||||
|
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
||||||
|
- name: Sign the images with GitHub OIDC Token
|
||||||
|
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
|
||||||
|
images+="${tag}@${DIGEST} "
|
||||||
|
done
|
||||||
|
cosign sign --yes ${images}
|
||||||
|
|
||||||
|
- name: Update repo description
|
||||||
|
uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
repository: vectorim/element-web
|
||||||
79
.github/workflows/dockerhub.yaml
vendored
79
.github/workflows/dockerhub.yaml
vendored
@@ -1,79 +0,0 @@
|
|||||||
name: Dockerhub
|
|
||||||
on:
|
|
||||||
workflow_dispatch: {}
|
|
||||||
push:
|
|
||||||
tags: [v*]
|
|
||||||
schedule:
|
|
||||||
# This job can take a while, and we have usage limits, so just publish develop only twice a day
|
|
||||||
- cron: "0 7/12 * * *"
|
|
||||||
concurrency: ${{ github.workflow }}-${{ github.ref_name }}
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
|
||||||
buildx:
|
|
||||||
name: Docker Buildx
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
environment: dockerhub
|
|
||||||
permissions:
|
|
||||||
id-token: write # needed for signing the images with GitHub OIDC Token
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3
|
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
vectorim/element-web
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
flavor: |
|
|
||||||
latest=${{ contains(github.ref_name, '-rc.') && 'false' || 'auto' }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
id: build-and-push
|
|
||||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Sign the images with GitHub OIDC Token
|
|
||||||
env:
|
|
||||||
DIGEST: ${{ steps.build-and-push.outputs.digest }}
|
|
||||||
TAGS: ${{ steps.meta.outputs.tags }}
|
|
||||||
run: |
|
|
||||||
images=""
|
|
||||||
for tag in ${TAGS}; do
|
|
||||||
images+="${tag}@${DIGEST} "
|
|
||||||
done
|
|
||||||
cosign sign --yes ${images}
|
|
||||||
|
|
||||||
- name: Update repo description
|
|
||||||
uses: peter-evans/dockerhub-description@e98e4d1628a5f3be2be7c231e50981aee98723ae # v4
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
repository: vectorim/element-web
|
|
||||||
14
.github/workflows/docs.yml
vendored
14
.github/workflows/docs.yml
vendored
@@ -17,23 +17,23 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Fetch element-desktop
|
- name: Fetch element-desktop
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
path: element-desktop
|
path: element-desktop
|
||||||
|
|
||||||
- name: Fetch element-web
|
- name: Fetch element-web
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
path: element-web
|
path: element-web
|
||||||
|
|
||||||
- name: Fetch matrix-js-sdk
|
- name: Fetch matrix-js-sdk
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
path: matrix-js-sdk
|
path: matrix-js-sdk
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: element-web/yarn.lock
|
cache-dependency-path: element-web/yarn.lock
|
||||||
@@ -47,7 +47,7 @@ jobs:
|
|||||||
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
|
echo "- [Automations](automations.md)" >> docs/SUMMARY.md
|
||||||
|
|
||||||
- name: Setup mdBook
|
- name: Setup mdBook
|
||||||
uses: peaceiris/actions-mdbook@v2
|
uses: peaceiris/actions-mdbook@ee69d230fe19748b7abf22df32acaa93833fad08 # v2
|
||||||
with:
|
with:
|
||||||
mdbook-version: "0.4.10"
|
mdbook-version: "0.4.10"
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
run: mdbook build
|
run: mdbook build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
|
||||||
with:
|
with:
|
||||||
path: ./book
|
path: ./book
|
||||||
|
|
||||||
@@ -104,4 +104,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Download HTML report
|
- name: Download HTML report
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
path: playwright-report
|
path: playwright-report
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
- name: 📤 Deploy to Netlify
|
||||||
uses: matrix-org/netlify-pr-preview@v3
|
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
||||||
with:
|
with:
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
|
|||||||
63
.github/workflows/end-to-end-tests.yaml
vendored
63
.github/workflows/end-to-end-tests.yaml
vendored
@@ -50,11 +50,11 @@ jobs:
|
|||||||
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
runners-matrix: ${{ steps.runner-vars.outputs.matrix }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
yarn build
|
yarn build
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: webapp
|
path: webapp
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Calculate runner variables
|
- name: Calculate runner variables
|
||||||
id: runner-vars
|
id: runner-vars
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
const numRunners = parseInt(process.env.NUM_RUNNERS, 10);
|
||||||
@@ -114,27 +114,33 @@ jobs:
|
|||||||
- Chrome
|
- Chrome
|
||||||
- Firefox
|
- Firefox
|
||||||
- WebKit
|
- WebKit
|
||||||
isCron:
|
- Dendrite
|
||||||
- ${{ github.event_name == 'schedule' }}
|
- Pinecone
|
||||||
# Skip the Firefox & Safari runs unless this was a cron trigger
|
runAllTests:
|
||||||
|
- ${{ github.event_name == 'schedule' || contains(github.event.pull_request.labels.*.name, 'X-Run-All-Tests') }}
|
||||||
|
# Skip the Firefox & Safari runs unless this was a cron trigger or PR has X-Run-All-Tests label
|
||||||
exclude:
|
exclude:
|
||||||
- isCron: false
|
- runAllTests: false
|
||||||
project: Firefox
|
project: Firefox
|
||||||
- isCron: false
|
- runAllTests: false
|
||||||
project: WebKit
|
project: WebKit
|
||||||
|
- runAllTests: false
|
||||||
|
project: Dendrite
|
||||||
|
- runAllTests: false
|
||||||
|
project: Pinecone
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: webapp
|
name: webapp
|
||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
cache-dependency-path: yarn.lock
|
cache-dependency-path: yarn.lock
|
||||||
@@ -148,12 +154,11 @@ jobs:
|
|||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
- name: Cache playwright binaries
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
id: playwright-cache
|
id: playwright-cache
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ~/.cache/ms-playwright
|
||||||
~/.cache/ms-playwright
|
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}
|
||||||
key: ${{ runner.os }}-playwright-${{ steps.playwright.outputs.version }}
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
||||||
@@ -170,29 +175,39 @@ jobs:
|
|||||||
yarn playwright test \
|
yarn playwright test \
|
||||||
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
|
--shard "${{ matrix.runner }}/${{ needs.build.outputs.num-runners }}" \
|
||||||
--project="${{ matrix.project }}" \
|
--project="${{ matrix.project }}" \
|
||||||
${{ github.event_name == 'pull_request' && '--grep-invert @mergequeue' || '' }}
|
${{ (github.event_name == 'pull_request' && matrix.runAllTests == false ) && '--grep-invert @mergequeue' || '' }}
|
||||||
|
|
||||||
- name: Upload blob report to GitHub Actions Artifacts
|
- name: Upload blob report to GitHub Actions Artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
|
name: all-blob-reports-${{ matrix.project }}-${{ matrix.runner }}
|
||||||
path: blob-report
|
path: blob-report
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
|
downstream-modules:
|
||||||
|
name: Downstream Playwright tests [element-modules]
|
||||||
|
needs: build
|
||||||
|
if: inputs.skip != true && github.event_name == 'merge_group'
|
||||||
|
uses: element-hq/element-modules/.github/workflows/reusable-playwright-tests.yml@main
|
||||||
|
with:
|
||||||
|
webapp-artifact: webapp
|
||||||
|
|
||||||
complete:
|
complete:
|
||||||
name: end-to-end-tests
|
name: end-to-end-tests
|
||||||
needs: playwright
|
needs:
|
||||||
|
- playwright
|
||||||
|
- downstream-modules
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -204,7 +219,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download blob reports from GitHub Actions Artifacts
|
- name: Download blob reports from GitHub Actions Artifacts
|
||||||
if: inputs.skip != true
|
if: inputs.skip != true
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
pattern: all-blob-reports-*
|
pattern: all-blob-reports-*
|
||||||
path: all-blob-reports
|
path: all-blob-reports
|
||||||
@@ -220,11 +235,11 @@ jobs:
|
|||||||
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
|
# Upload the HTML report even if one of our reporters fails, this can happen when stale screenshots are detected
|
||||||
- name: Upload HTML report
|
- name: Upload HTML report
|
||||||
if: always() && inputs.skip != true
|
if: always() && inputs.skip != true
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: html-report
|
name: html-report
|
||||||
path: playwright-report
|
path: playwright-report
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
- if: needs.playwright.result != 'skipped' && needs.playwright.result != 'success'
|
- if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
|||||||
4
.github/workflows/issue_closed.yml
vendored
4
.github/workflows/issue_closed.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
name: Tidy closed issues
|
name: Tidy closed issues
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
id: main
|
id: main
|
||||||
with:
|
with:
|
||||||
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
# PAT needed as the GITHUB_TOKEN won't be able to see cross-references from other orgs (matrix-org)
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
name: Close duplicate as Not Planned
|
name: Close duplicate as Not Planned
|
||||||
if: steps.main.outputs.closeAsNotPlanned
|
if: steps.main.outputs.closeAsNotPlanned
|
||||||
with:
|
with:
|
||||||
|
|||||||
4
.github/workflows/netlify.yaml
vendored
4
.github/workflows/netlify.yaml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
Exercise caution. Use test accounts.
|
Exercise caution. Use test accounts.
|
||||||
|
|
||||||
- name: 📥 Download artifact
|
- name: 📥 Download artifact
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
path: webapp
|
path: webapp
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
- name: 📤 Deploy to Netlify
|
||||||
uses: matrix-org/netlify-pr-preview@v3
|
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
||||||
with:
|
with:
|
||||||
path: webapp
|
path: webapp
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
|
|||||||
2
.github/workflows/pending-reviews.yaml
vendored
2
.github/workflows/pending-reviews.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
URL: "https://github.com/pulls?q=is%3Apr+is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+review-requested%3A%40me+sort%3Aupdated-desc+"
|
||||||
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
RELEASE_BLOCKERS_URL: "https://github.com/pulls?q=is%3Aopen+repo%3Amatrix-org%2Fmatrix-js-sdk+repo%3Amatrix-org%2Fmatrix-react-sdk+repo%3Aelement-hq%2Felement-web+repo%3Aelement-hq%2Felement-desktop+sort%3Aupdated-desc+label%3AX-Release-Blocker+"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
ROOM_ID: ${{ secrets.ROOM_ID }}
|
ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
|
|||||||
@@ -10,20 +10,20 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- name: Update synapse image
|
- name: Update synapse image
|
||||||
run: |
|
run: |
|
||||||
docker pull "$IMAGE"
|
docker pull "$IMAGE"
|
||||||
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
INSPECT=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE")
|
||||||
DIGEST=${INSPECT#*@}
|
DIGEST=${INSPECT#*@}
|
||||||
sed -i "s/const DOCKER_TAG.*/const DOCKER_TAG = \"develop@$DIGEST\";/" playwright/plugins/homeserver/synapse/index.ts
|
sed -i "s/const TAG.*/const TAG = \"develop@$DIGEST\";/" playwright/testcontainers/synapse.ts
|
||||||
env:
|
env:
|
||||||
IMAGE: ghcr.io/element-hq/synapse:develop
|
IMAGE: ghcr.io/element-hq/synapse:develop
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/playwright-image-updates
|
branch: actions/playwright-image-updates
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
name: Check PR base branch
|
name: Check PR base branch
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const baseBranch = context.payload.pull_request.base.ref;
|
const baseBranch = context.payload.pull_request.base.ref;
|
||||||
|
|||||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -19,6 +19,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: read
|
pull-requests: read
|
||||||
|
id-token: write
|
||||||
secrets:
|
secrets:
|
||||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
@@ -50,7 +51,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
checks: read
|
checks: read
|
||||||
steps:
|
steps:
|
||||||
- name: Wait for dockerhub
|
- name: Wait for docker build
|
||||||
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
uses: t3chguy/wait-on-check-action@18541021811b56544d90e0f073401c2b99e249d6 # fork
|
||||||
with:
|
with:
|
||||||
ref: master
|
ref: master
|
||||||
|
|||||||
12
.github/workflows/release_prepare.yml
vendored
12
.github/workflows/release_prepare.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
|||||||
REPOS: matrix-js-sdk element-web element-desktop
|
REPOS: matrix-js-sdk element-web element-desktop
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Element Desktop
|
- name: Checkout Element Desktop
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.element-desktop
|
if: inputs.element-desktop
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-desktop
|
repository: element-hq/element-desktop
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Element Web
|
- name: Checkout Element Web
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.element-web
|
if: inputs.element-web
|
||||||
with:
|
with:
|
||||||
repository: element-hq/element-web
|
repository: element-hq/element-web
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
- name: Checkout Matrix JS SDK
|
- name: Checkout Matrix JS SDK
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
if: inputs.matrix-js-sdk
|
if: inputs.matrix-js-sdk
|
||||||
with:
|
with:
|
||||||
repository: matrix-org/matrix-js-sdk
|
repository: matrix-org/matrix-js-sdk
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
repo: matrix-org/matrix-js-sdk
|
repo: matrix-org/matrix-js-sdk
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: draft
|
check-name: "draft / draft"
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|
||||||
- name: Wait for element-web draft
|
- name: Wait for element-web draft
|
||||||
@@ -111,7 +111,7 @@ jobs:
|
|||||||
repo: element-hq/element-web
|
repo: element-hq/element-web
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: draft
|
check-name: "draft / draft"
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|
||||||
- name: Wait for element-desktop draft
|
- name: Wait for element-desktop draft
|
||||||
@@ -122,5 +122,5 @@ jobs:
|
|||||||
repo: element-hq/element-desktop
|
repo: element-hq/element-desktop
|
||||||
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
wait-interval: 10
|
wait-interval: 10
|
||||||
check-name: draft
|
check-name: "draft / draft"
|
||||||
allowed-conclusions: success
|
allowed-conclusions: success
|
||||||
|
|||||||
29
.github/workflows/static_analysis.yaml
vendored
29
.github/workflows/static_analysis.yaml
vendored
@@ -23,9 +23,9 @@ jobs:
|
|||||||
name: "Typescript Syntax Check"
|
name: "Typescript Syntax Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -51,12 +51,13 @@ jobs:
|
|||||||
error|invalid_json
|
error|invalid_json
|
||||||
error|misconfigured
|
error|misconfigured
|
||||||
welcome_to_element
|
welcome_to_element
|
||||||
|
devtools|settings|elementCallUrl
|
||||||
|
|
||||||
rethemendex_lint:
|
rethemendex_lint:
|
||||||
name: "Rethemendex Check"
|
name: "Rethemendex Check"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- run: ./res/css/rethemendex.sh
|
- run: ./res/css/rethemendex.sh
|
||||||
|
|
||||||
@@ -66,9 +67,9 @@ jobs:
|
|||||||
name: "ESLint"
|
name: "ESLint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -84,9 +85,9 @@ jobs:
|
|||||||
name: "Style Lint"
|
name: "Style Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -102,9 +103,9 @@ jobs:
|
|||||||
name: "Workflow Lint"
|
name: "Workflow Lint"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -120,9 +121,9 @@ jobs:
|
|||||||
name: "Analyse Dead Code"
|
name: "Analyse Dead Code"
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -132,9 +133,3 @@ jobs:
|
|||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: "yarn run lint:knip"
|
run: "yarn run lint:knip"
|
||||||
|
|
||||||
- name: Install Deps
|
|
||||||
run: "scripts/layered.sh"
|
|
||||||
|
|
||||||
- name: Dead Code Analysis
|
|
||||||
run: "yarn run analyse:unused-exports"
|
|
||||||
|
|||||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -39,12 +39,12 @@ jobs:
|
|||||||
runner: [1, 2]
|
runner: [1, 2]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||||
|
|
||||||
- name: Yarn cache
|
- name: Yarn cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
@@ -55,7 +55,7 @@ jobs:
|
|||||||
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
JS_SDK_GITHUB_BASE_REF: ${{ inputs.matrix-js-sdk-sha }}
|
||||||
|
|
||||||
- name: Jest Cache
|
- name: Jest Cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
||||||
with:
|
with:
|
||||||
path: /tmp/jest_cache
|
path: /tmp/jest_cache
|
||||||
key: ${{ hashFiles('**/yarn.lock') }}
|
key: ${{ hashFiles('**/yarn.lock') }}
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
if: env.ENABLE_COVERAGE == 'true'
|
if: env.ENABLE_COVERAGE == 'true'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.runner }}
|
name: coverage-${{ matrix.runner }}
|
||||||
path: |
|
path: |
|
||||||
@@ -104,7 +104,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Skip SonarCloud in merge queue
|
- name: Skip SonarCloud in merge queue
|
||||||
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
|
||||||
uses: guibranco/github-status-action-v2@d469d49426f5a7b8a1fbcac20ad274d3e4892321
|
uses: guibranco/github-status-action-v2@5f2b01ce1394109f70954ae6b69ef41cf7928e63
|
||||||
with:
|
with:
|
||||||
authToken: ${{ secrets.GITHUB_TOKEN }}
|
authToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
state: success
|
state: success
|
||||||
|
|||||||
3
.github/workflows/triage-assigned.yml
vendored
3
.github/workflows/triage-assigned.yml
vendored
@@ -11,7 +11,8 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: |
|
if: |
|
||||||
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
contains(github.event.issue.assignees.*.login, 't3chguy') ||
|
||||||
contains(github.event.issue.assignees.*.login, 'andybalaam') ||
|
contains(github.event.issue.assignees.*.login, 'florianduros') ||
|
||||||
|
contains(github.event.issue.assignees.*.login, 'dbkr') ||
|
||||||
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
contains(github.event.issue.assignees.*.login, 'MidhunSureshR')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@main
|
- uses: actions/add-to-project@main
|
||||||
|
|||||||
16
.github/workflows/triage-labelled.yml
vendored
16
.github/workflows/triage-labelled.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
contains(github.event.issue.labels.*.name, 'A-Rich-Text-Editor') ||
|
||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -44,7 +44,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
contains(github.event.issue.labels.*.name, 'good first issue') ||
|
||||||
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
contains(github.event.issue.labels.*.name, 'Hacktoberfest')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.addLabels({
|
github.rest.issues.addLabels({
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
contains(github.event.issue.labels.*.name, 'X-Needs-Info')
|
||||||
steps:
|
steps:
|
||||||
- id: add_to_project
|
- id: add_to_project
|
||||||
uses: actions/add-to-project@v1.0.2
|
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: ${{ env.PROJECT_URL }}
|
project-url: ${{ env.PROJECT_URL }}
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -84,7 +84,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
contains(github.event.issue.labels.*.name, 'Z-Flaky-Test')
|
||||||
steps:
|
steps:
|
||||||
- id: add_to_project
|
- id: add_to_project
|
||||||
uses: actions/add-to-project@v1.0.2
|
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||||
with:
|
with:
|
||||||
project-url: ${{ env.PROJECT_URL }}
|
project-url: ${{ env.PROJECT_URL }}
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
@@ -150,15 +150,15 @@ jobs:
|
|||||||
project-url: https://github.com/orgs/element-hq/projects/41
|
project-url: https://github.com/orgs/element-hq/projects/41
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
verticals_feature:
|
crypto:
|
||||||
name: Add labelled issues to Verticals Feature project
|
name: Add labelled issues to Crypto project
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
if: >
|
if: >
|
||||||
contains(github.event.issue.labels.*.name, 'Team: Verticals Feature')
|
contains(github.event.issue.labels.*.name, 'Team: Crypto')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/add-to-project@main
|
- uses: actions/add-to-project@main
|
||||||
with:
|
with:
|
||||||
project-url: https://github.com/orgs/element-hq/projects/57
|
project-url: https://github.com/orgs/element-hq/projects/76
|
||||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
|
|
||||||
tech_debt:
|
tech_debt:
|
||||||
|
|||||||
19
.github/workflows/triage-stale-flaky-tests.yml
vendored
19
.github/workflows/triage-stale-flaky-tests.yml
vendored
@@ -1,19 +0,0 @@
|
|||||||
name: Close stale flaky issues
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
|
||||||
close:
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: write
|
|
||||||
issues: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v9
|
|
||||||
with:
|
|
||||||
only-labels: "Z-Flaky-Test"
|
|
||||||
days-before-stale: 14
|
|
||||||
days-before-close: 0
|
|
||||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
|
||||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
|
||||||
27
.github/workflows/triage-stale.yml
vendored
Normal file
27
.github/workflows/triage-stale.yml
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
name: Close stale issues & PRs
|
||||||
|
on:
|
||||||
|
workflow_dispatch: {}
|
||||||
|
schedule:
|
||||||
|
- cron: "30 1 * * *"
|
||||||
|
permissions: {}
|
||||||
|
jobs:
|
||||||
|
close:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||||
|
with:
|
||||||
|
operations-per-run: 100
|
||||||
|
# Flaky test issue closing
|
||||||
|
only-issue-labels: "Z-Flaky-Test"
|
||||||
|
days-before-issue-stale: 14
|
||||||
|
days-before-issue-close: 0
|
||||||
|
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||||
|
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||||
|
# Stale PR closing
|
||||||
|
days-before-pr-stale: 180
|
||||||
|
days-before-pr-close: 0
|
||||||
|
close-pr-message: "This PR has been automatically closed because it has been stale for 180 days. If you wish to continue working on this PR, please ping a maintainer to reopen it."
|
||||||
2
.github/workflows/triage-unlabelled.yml
vendored
2
.github/workflows/triage-unlabelled.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
contains(github.event.issue.labels.*.name, 'A-Element-Call')) &&
|
||||||
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
contains(github.event.issue.labels.*.name, 'Z-Labs')
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
github.rest.issues.removeLabel({
|
||||||
|
|||||||
6
.github/workflows/update-jitsi.yml
vendored
6
.github/workflows/update-jitsi.yml
vendored
@@ -9,9 +9,9 @@ jobs:
|
|||||||
update:
|
update:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
run: "yarn update:jitsi"
|
run: "yarn update:jitsi"
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||||
branch: actions/jitsi-update
|
branch: actions/jitsi-update
|
||||||
|
|||||||
2
.github/workflows/update-topics.yaml
vendored
2
.github/workflows/update-topics.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
environment: Matrix
|
environment: Matrix
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
HS_URL: ${{ secrets.BETABOT_HS_URL }}
|
||||||
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
LOBBY_ROOM_ID: ${{ secrets.ROOM_ID }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,7 +25,7 @@ electron/pub
|
|||||||
.env
|
.env
|
||||||
/coverage
|
/coverage
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
/src/modules.ts
|
/src/modules.js
|
||||||
/build_config.yaml
|
/build_config.yaml
|
||||||
/book
|
/book
|
||||||
/index.html
|
/index.html
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ electron/pub
|
|||||||
/coverage
|
/coverage
|
||||||
# Auto-generated file
|
# Auto-generated file
|
||||||
/src/modules.ts
|
/src/modules.ts
|
||||||
|
/src/modules.js
|
||||||
/src/i18n/strings
|
/src/i18n/strings
|
||||||
/build_config.yaml
|
/build_config.yaml
|
||||||
# Raises an error because it contains a template var breaking the script tag
|
# Raises an error because it contains a template var breaking the script tag
|
||||||
|
|||||||
@@ -33,19 +33,15 @@ module.exports = {
|
|||||||
"import-notation": null,
|
"import-notation": null,
|
||||||
"value-keyword-case": null,
|
"value-keyword-case": null,
|
||||||
"declaration-block-no-redundant-longhand-properties": null,
|
"declaration-block-no-redundant-longhand-properties": null,
|
||||||
"declaration-block-no-duplicate-properties": [
|
|
||||||
true,
|
|
||||||
// useful for fallbacks
|
|
||||||
{ ignore: ["consecutive-duplicates-with-different-values"] },
|
|
||||||
],
|
|
||||||
"shorthand-property-no-redundant-values": null,
|
"shorthand-property-no-redundant-values": null,
|
||||||
"property-no-vendor-prefix": null,
|
"property-no-vendor-prefix": null,
|
||||||
"value-no-vendor-prefix": null,
|
|
||||||
"selector-no-vendor-prefix": null,
|
"selector-no-vendor-prefix": null,
|
||||||
"media-feature-name-no-vendor-prefix": null,
|
"media-feature-name-no-vendor-prefix": null,
|
||||||
"number-max-precision": null,
|
"number-max-precision": null,
|
||||||
"no-invalid-double-slash-comments": true,
|
"no-invalid-double-slash-comments": true,
|
||||||
"media-feature-range-notation": null,
|
"media-feature-range-notation": null,
|
||||||
|
"declaration-property-value-no-unknown": null,
|
||||||
|
"declaration-property-value-keyword-no-deprecated": null,
|
||||||
"csstools/value-no-unknown-custom-properties": [
|
"csstools/value-no-unknown-custom-properties": [
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
|
|||||||
325
CHANGELOG.md
325
CHANGELOG.md
@@ -1,3 +1,328 @@
|
|||||||
|
Changes in [1.11.103](https://github.com/element-hq/element-web/releases/tag/v1.11.103) (2025-06-10)
|
||||||
|
====================================================================================================
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
+ Check the sender of an event matches owner of session, preventing sender spoofing by homeserver owners.
|
||||||
|
[13c1d20](https://github.com/matrix-org/matrix-rust-sdk/commit/13c1d2048286bbabf5e7bc6b015aafee98f04d55) (High, [GHSA-x958-rvg6-956w](https://github.com/matrix-org/matrix-rust-sdk/security/advisories/GHSA-x958-rvg6-956w)).
|
||||||
|
|
||||||
|
Changes in [1.11.102](https://github.com/element-hq/element-web/releases/tag/v1.11.102) (2025-06-03)
|
||||||
|
====================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* EW: Modernize the recovery key input modal ([#29819](https://github.com/element-hq/element-web/pull/29819)). Contributed by @uhoreg.
|
||||||
|
* New room list: move secondary filters into primary filters ([#29972](https://github.com/element-hq/element-web/pull/29972)). Contributed by @florianduros.
|
||||||
|
* Prompt the user when key storage is unexpectedly off ([#29912](https://github.com/element-hq/element-web/pull/29912)). Contributed by @andybalaam.
|
||||||
|
* New room list: move sort menu in room list header ([#29983](https://github.com/element-hq/element-web/pull/29983)). Contributed by @florianduros.
|
||||||
|
* New room list: rework spacing of room list item ([#29965](https://github.com/element-hq/element-web/pull/29965)). Contributed by @florianduros.
|
||||||
|
* RLS: Remove forgotten room from skiplist ([#29933](https://github.com/element-hq/element-web/pull/29933)). Contributed by @MidhunSureshR.
|
||||||
|
* Add room list sorting ([#29951](https://github.com/element-hq/element-web/pull/29951)). Contributed by @dbkr.
|
||||||
|
* Don't use the minimised width(68px) on the new room list ([#29778](https://github.com/element-hq/element-web/pull/29778)). Contributed by @langleyd.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* [Backport staging] Close call options popup menu when option has been selected ([#30054](https://github.com/element-hq/element-web/pull/30054)). Contributed by @RiotRobot.
|
||||||
|
* RoomListStoreV3: Only add new rooms that pass `VisibilityProvider` check ([#29974](https://github.com/element-hq/element-web/pull/29974)). Contributed by @MidhunSureshR.
|
||||||
|
* Re-order primary filters ([#29957](https://github.com/element-hq/element-web/pull/29957)). Contributed by @dbkr.
|
||||||
|
* Fix leaky CSS adding `!` to all H1 elements ([#29964](https://github.com/element-hq/element-web/pull/29964)). Contributed by @t3chguy.
|
||||||
|
* Fix extensions panel style ([#29273](https://github.com/element-hq/element-web/pull/29273)). Contributed by @langleyd.
|
||||||
|
* Fix state events being hidden from widgets in read\_events actions ([#29954](https://github.com/element-hq/element-web/pull/29954)). Contributed by @robintown.
|
||||||
|
* Remove old filter test ([#29963](https://github.com/element-hq/element-web/pull/29963)). Contributed by @dbkr.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.101](https://github.com/element-hq/element-web/releases/tag/v1.11.101) (2025-05-20)
|
||||||
|
====================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* New room list: add keyboard navigation support ([#29805](https://github.com/element-hq/element-web/pull/29805)). Contributed by @florianduros.
|
||||||
|
* Use the JoinRuleSettings component for the guest link access prompt. ([#28614](https://github.com/element-hq/element-web/pull/28614)). Contributed by @toger5.
|
||||||
|
* Add loading state to the new room list view ([#29725](https://github.com/element-hq/element-web/pull/29725)). Contributed by @langleyd.
|
||||||
|
* Make OIDC identity reset consistent with EX ([#29854](https://github.com/element-hq/element-web/pull/29854)). Contributed by @andybalaam.
|
||||||
|
* Support error code for email / phone adding unsupported (MSC4178) ([#29855](https://github.com/element-hq/element-web/pull/29855)). Contributed by @dbkr.
|
||||||
|
* Update identity reset UI (Make consistent with EX) ([#29701](https://github.com/element-hq/element-web/pull/29701)). Contributed by @andybalaam.
|
||||||
|
* Add secondary filters to the new room list ([#29818](https://github.com/element-hq/element-web/pull/29818)). Contributed by @dbkr.
|
||||||
|
* Fix battery drain from Web Audio ([#29203](https://github.com/element-hq/element-web/pull/29203)). Contributed by @mbachry.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix go home shortcut on macos and change toggle action events shortcut ([#29929](https://github.com/element-hq/element-web/pull/29929)). Contributed by @florianduros.
|
||||||
|
* New room list: fix outdated message preview when space or filter change ([#29925](https://github.com/element-hq/element-web/pull/29925)). Contributed by @florianduros.
|
||||||
|
* Stop migrating to MSC4278 if the config exists. ([#29924](https://github.com/element-hq/element-web/pull/29924)). Contributed by @Half-Shot.
|
||||||
|
* Ensure consistent download file name on download from ImageView ([#29913](https://github.com/element-hq/element-web/pull/29913)). Contributed by @t3chguy.
|
||||||
|
* Add error toast when service worker registration fails ([#29895](https://github.com/element-hq/element-web/pull/29895)). Contributed by @t3chguy.
|
||||||
|
* New Room List: Prevent old tombstoned rooms from appearing in the list ([#29881](https://github.com/element-hq/element-web/pull/29881)). Contributed by @MidhunSureshR.
|
||||||
|
* Remove lag in search field ([#29885](https://github.com/element-hq/element-web/pull/29885)). Contributed by @florianduros.
|
||||||
|
* Respect UIFeature.Voip ([#29873](https://github.com/element-hq/element-web/pull/29873)). Contributed by @langleyd.
|
||||||
|
* Allow jumping to message search from spotlight ([#29850](https://github.com/element-hq/element-web/pull/29850)). Contributed by @t3chguy.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.100](https://github.com/element-hq/element-web/releases/tag/v1.11.100) (2025-05-06)
|
||||||
|
====================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Move rich topics out of labs / stabilise MSC3765 ([#29817](https://github.com/element-hq/element-web/pull/29817)). Contributed by @Johennes.
|
||||||
|
* Spell out that Element Web does \*not\* work on mobile. ([#29211](https://github.com/element-hq/element-web/pull/29211)). Contributed by @ara4n.
|
||||||
|
* Add message preview support to the new room list ([#29784](https://github.com/element-hq/element-web/pull/29784)). Contributed by @dbkr.
|
||||||
|
* Global configuration flag for media previews ([#29582](https://github.com/element-hq/element-web/pull/29582)). Contributed by @Half-Shot.
|
||||||
|
* New room list: add partial keyboard shortcuts support ([#29783](https://github.com/element-hq/element-web/pull/29783)). Contributed by @florianduros.
|
||||||
|
* MVVM RoomSummaryCard Topic ([#29710](https://github.com/element-hq/element-web/pull/29710)). Contributed by @MarcWadai.
|
||||||
|
* Warn on self change from settings > roles ([#28926](https://github.com/element-hq/element-web/pull/28926)). Contributed by @MarcWadai.
|
||||||
|
* New room list: new visual for invitation ([#29773](https://github.com/element-hq/element-web/pull/29773)). Contributed by @florianduros.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix incorrect display of the user info display name ([#29826](https://github.com/element-hq/element-web/pull/29826)). Contributed by @langleyd.
|
||||||
|
* RoomListStore: Remove invite rooms on decline ([#29804](https://github.com/element-hq/element-web/pull/29804)). Contributed by @MidhunSureshR.
|
||||||
|
* Fix the buttons not being displayed with long preview text ([#29811](https://github.com/element-hq/element-web/pull/29811)). Contributed by @dbkr.
|
||||||
|
* New room list: fix missing/incorrect notification decoration ([#29796](https://github.com/element-hq/element-web/pull/29796)). Contributed by @florianduros.
|
||||||
|
* New Room List: Prevent potential scroll jump/flicker when switching spaces ([#29781](https://github.com/element-hq/element-web/pull/29781)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: fix incorrect decoration ([#29770](https://github.com/element-hq/element-web/pull/29770)). Contributed by @florianduros.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.99](https://github.com/element-hq/element-web/releases/tag/v1.11.99) (2025-04-23)
|
||||||
|
==================================================================================================
|
||||||
|
No changes, just bumping the version to accommodate a new Element Desktop release
|
||||||
|
|
||||||
|
Changes in [1.11.98](https://github.com/element-hq/element-web/releases/tag/v1.11.98) (2025-04-22)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* print better errors in the search view instead of a blocking modal ([#29724](https://github.com/element-hq/element-web/pull/29724)). Contributed by @Jujure.
|
||||||
|
* New room list: video room and video call decoration ([#29693](https://github.com/element-hq/element-web/pull/29693)). Contributed by @florianduros.
|
||||||
|
* Remove Secure Backup, Cross-signing and Cryptography sections in `Security & Privacy` user settings ([#29088](https://github.com/element-hq/element-web/pull/29088)). Contributed by @florianduros.
|
||||||
|
* Allow reporting a room when rejecting an invite. ([#29570](https://github.com/element-hq/element-web/pull/29570)). Contributed by @Half-Shot.
|
||||||
|
* RoomListViewModel: Reset primary and secondary filters on space change ([#29672](https://github.com/element-hq/element-web/pull/29672)). Contributed by @MidhunSureshR.
|
||||||
|
* RoomListStore: Support specific sorting requirements for muted rooms ([#29665](https://github.com/element-hq/element-web/pull/29665)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: add notification options menu ([#29639](https://github.com/element-hq/element-web/pull/29639)). Contributed by @florianduros.
|
||||||
|
* Room List: Scroll to top of the list when active room is not in the list ([#29650](https://github.com/element-hq/element-web/pull/29650)). Contributed by @MidhunSureshR.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix unwanted form submit behaviour in memberlist ([#29747](https://github.com/element-hq/element-web/pull/29747)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: fix public room icon visibility when filter change ([#29737](https://github.com/element-hq/element-web/pull/29737)). Contributed by @florianduros.
|
||||||
|
* Fix custom theme support for short hex \& rgba hex strings ([#29726](https://github.com/element-hq/element-web/pull/29726)). Contributed by @t3chguy.
|
||||||
|
* New room list: minor visual fixes ([#29723](https://github.com/element-hq/element-web/pull/29723)). Contributed by @florianduros.
|
||||||
|
* Fix getOidcCallbackUrl for Element Desktop ([#29711](https://github.com/element-hq/element-web/pull/29711)). Contributed by @t3chguy.
|
||||||
|
* Fix some webp images improperly marked as animated ([#29713](https://github.com/element-hq/element-web/pull/29713)). Contributed by @Petersmit27.
|
||||||
|
* Revert deletion of hydrateSession ([#29703](https://github.com/element-hq/element-web/pull/29703)). Contributed by @Jujure.
|
||||||
|
* Fix converttoroom \& converttodm not working ([#29705](https://github.com/element-hq/element-web/pull/29705)). Contributed by @t3chguy.
|
||||||
|
* Ensure forceCloseAllModals also closes priority/static modals ([#29706](https://github.com/element-hq/element-web/pull/29706)). Contributed by @t3chguy.
|
||||||
|
* Continue button is disabled when uploading a recovery key file ([#29695](https://github.com/element-hq/element-web/pull/29695)). Contributed by @Giwayume.
|
||||||
|
* Catch errors after syncing recovery ([#29691](https://github.com/element-hq/element-web/pull/29691)). Contributed by @andybalaam.
|
||||||
|
* New room list: fix multiple visual issues ([#29673](https://github.com/element-hq/element-web/pull/29673)). Contributed by @florianduros.
|
||||||
|
* New Room List: Fix mentions filter matching rooms with any highlight ([#29668](https://github.com/element-hq/element-web/pull/29668)). Contributed by @MidhunSureshR.
|
||||||
|
* Fix truncated emoji label during emoji SAS ([#29643](https://github.com/element-hq/element-web/pull/29643)). Contributed by @florianduros.
|
||||||
|
* Remove duplicate jitsi link ([#29642](https://github.com/element-hq/element-web/pull/29642)). Contributed by @dbkr.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.97](https://github.com/element-hq/element-web/releases/tag/v1.11.97) (2025-04-08)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* New room list: reduce padding between avatar and room list border ([#29634](https://github.com/element-hq/element-web/pull/29634)). Contributed by @florianduros.
|
||||||
|
* Bundle Element Call with Element Web packages ([#29309](https://github.com/element-hq/element-web/pull/29309)). Contributed by @t3chguy.
|
||||||
|
* Hide an event notification if it is redacted ([#29605](https://github.com/element-hq/element-web/pull/29605)). Contributed by @Half-Shot.
|
||||||
|
* Docker: Use nginx-unprivileged as base image ([#29353](https://github.com/element-hq/element-web/pull/29353)). Contributed by @AndrewFerr.
|
||||||
|
* Switch away from nesting React trees and mangling the DOM ([#29586](https://github.com/element-hq/element-web/pull/29586)). Contributed by @t3chguy.
|
||||||
|
* New room list: add notification decoration ([#29552](https://github.com/element-hq/element-web/pull/29552)). Contributed by @florianduros.
|
||||||
|
* RoomListStore: Unread filter should match rooms that were marked as unread ([#29580](https://github.com/element-hq/element-web/pull/29580)). Contributed by @MidhunSureshR.
|
||||||
|
* Add support for hiding videos ([#29496](https://github.com/element-hq/element-web/pull/29496)). Contributed by @Half-Shot.
|
||||||
|
* Use an outline icon for the report room button ([#29573](https://github.com/element-hq/element-web/pull/29573)). Contributed by @robintown.
|
||||||
|
* Generate/load pickle key on SSO ([#29568](https://github.com/element-hq/element-web/pull/29568)). Contributed by @Jujure.
|
||||||
|
* Add report room dialog button/dialog. ([#29513](https://github.com/element-hq/element-web/pull/29513)). Contributed by @Half-Shot.
|
||||||
|
* RoomListViewModel: Make the active room sticky in the list ([#29551](https://github.com/element-hq/element-web/pull/29551)). Contributed by @MidhunSureshR.
|
||||||
|
* Replace checkboxes with Compound checkboxes, and appropriately label each checkbox. ([#29363](https://github.com/element-hq/element-web/pull/29363)). Contributed by @Half-Shot.
|
||||||
|
* New room list: add selection decoration ([#29531](https://github.com/element-hq/element-web/pull/29531)). Contributed by @florianduros.
|
||||||
|
* Simplified Sliding Sync ([#28515](https://github.com/element-hq/element-web/pull/28515)). Contributed by @dbkr.
|
||||||
|
* Add ability to hide images after clicking "show image" ([#29467](https://github.com/element-hq/element-web/pull/29467)). Contributed by @Half-Shot.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix scroll issues in memberlist ([#29392](https://github.com/element-hq/element-web/pull/29392)). Contributed by @MidhunSureshR.
|
||||||
|
* Ensure clicks on spoilers do not get handled by the hidden content ([#29618](https://github.com/element-hq/element-web/pull/29618)). Contributed by @t3chguy.
|
||||||
|
* New room list: add cursor pointer on room list item ([#29627](https://github.com/element-hq/element-web/pull/29627)). Contributed by @florianduros.
|
||||||
|
* Fix missing ambiguous url tooltips on Element Desktop ([#29619](https://github.com/element-hq/element-web/pull/29619)). Contributed by @t3chguy.
|
||||||
|
* New room list: fix spacing and padding ([#29607](https://github.com/element-hq/element-web/pull/29607)). Contributed by @florianduros.
|
||||||
|
* Make fetchdep check out matching branch name ([#29601](https://github.com/element-hq/element-web/pull/29601)). Contributed by @dbkr.
|
||||||
|
* Fix MFileBody fileName not considering `filename` ([#29589](https://github.com/element-hq/element-web/pull/29589)). Contributed by @t3chguy.
|
||||||
|
* Fix token expiry racing with login causing wrong error to be shown ([#29566](https://github.com/element-hq/element-web/pull/29566)). Contributed by @t3chguy.
|
||||||
|
* Fix bug which caused startup to hang if the clock was wound back since a previous session ([#29558](https://github.com/element-hq/element-web/pull/29558)). Contributed by @richvdh.
|
||||||
|
* RoomListViewModel: Reset any primary filter on secondary filter change ([#29562](https://github.com/element-hq/element-web/pull/29562)). Contributed by @MidhunSureshR.
|
||||||
|
* RoomListStore: Unread filter should only filter rooms having unread counts ([#29555](https://github.com/element-hq/element-web/pull/29555)). Contributed by @MidhunSureshR.
|
||||||
|
* In force-verify mode, prevent bypassing by cancelling device verification ([#29487](https://github.com/element-hq/element-web/pull/29487)). Contributed by @andybalaam.
|
||||||
|
* Add title attribute to user identifier ([#29547](https://github.com/element-hq/element-web/pull/29547)). Contributed by @arpitbatra123.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.96](https://github.com/element-hq/element-web/releases/tag/v1.11.96) (2025-03-25)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* RoomListViewModel: Track the index of the active room in the list ([#29519](https://github.com/element-hq/element-web/pull/29519)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: add empty state ([#29512](https://github.com/element-hq/element-web/pull/29512)). Contributed by @florianduros.
|
||||||
|
* Implement `MessagePreviewViewModel` ([#29514](https://github.com/element-hq/element-web/pull/29514)). Contributed by @MidhunSureshR.
|
||||||
|
* RoomListViewModel: Add functionality to toggle message preview setting ([#29511](https://github.com/element-hq/element-web/pull/29511)). Contributed by @MidhunSureshR.
|
||||||
|
* New room list: add more options menu on room list item ([#29445](https://github.com/element-hq/element-web/pull/29445)). Contributed by @florianduros.
|
||||||
|
* RoomListViewModel: Provide a way to resort the room list and track the active sort method ([#29499](https://github.com/element-hq/element-web/pull/29499)). Contributed by @MidhunSureshR.
|
||||||
|
* Change \*All rooms\* meta space name to \*All Chats\* ([#29498](https://github.com/element-hq/element-web/pull/29498)). Contributed by @florianduros.
|
||||||
|
* Add setting to hide avatars of rooms you have been invited to. ([#29497](https://github.com/element-hq/element-web/pull/29497)). Contributed by @Half-Shot.
|
||||||
|
* Room List Store: Save preferred sorting algorithm and use that on app launch ([#29493](https://github.com/element-hq/element-web/pull/29493)). Contributed by @MidhunSureshR.
|
||||||
|
* Add key storage toggle to Encryption settings ([#29310](https://github.com/element-hq/element-web/pull/29310)). Contributed by @dbkr.
|
||||||
|
* New room list: add primary filters ([#29481](https://github.com/element-hq/element-web/pull/29481)). Contributed by @florianduros.
|
||||||
|
* Implement MSC4142: Remove unintentional intentional mentions in replies ([#28209](https://github.com/element-hq/element-web/pull/28209)). Contributed by @tulir.
|
||||||
|
* White background for 'They do not match' button ([#29470](https://github.com/element-hq/element-web/pull/29470)). Contributed by @andybalaam.
|
||||||
|
* RoomListViewModel: Support secondary filters in the view model ([#29465](https://github.com/element-hq/element-web/pull/29465)). Contributed by @MidhunSureshR.
|
||||||
|
* RoomListViewModel: Support primary filters in the view model ([#29454](https://github.com/element-hq/element-web/pull/29454)). Contributed by @MidhunSureshR.
|
||||||
|
* Room List Store: Implement secondary filters ([#29458](https://github.com/element-hq/element-web/pull/29458)). Contributed by @MidhunSureshR.
|
||||||
|
* Room List Store: Implement rest of the primary filters ([#29444](https://github.com/element-hq/element-web/pull/29444)). Contributed by @MidhunSureshR.
|
||||||
|
* Room List Store: Support filters by implementing just the favourite filter ([#29433](https://github.com/element-hq/element-web/pull/29433)). Contributed by @MidhunSureshR.
|
||||||
|
* Move toggle switch for integration manager for a11y ([#29436](https://github.com/element-hq/element-web/pull/29436)). Contributed by @Half-Shot.
|
||||||
|
* New room list: basic flat list ([#29368](https://github.com/element-hq/element-web/pull/29368)). Contributed by @florianduros.
|
||||||
|
* Improve rageshake upload experience by providing useful error information ([#29378](https://github.com/element-hq/element-web/pull/29378)). Contributed by @Half-Shot.
|
||||||
|
* Add more functionality to the room list vm ([#29402](https://github.com/element-hq/element-web/pull/29402)). Contributed by @MidhunSureshR.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* New room list: fix compose menu action in space ([#29500](https://github.com/element-hq/element-web/pull/29500)). Contributed by @florianduros.
|
||||||
|
* Change ToggleHiddenEventVisibility \& GoToHome KeyBindingActions ([#29374](https://github.com/element-hq/element-web/pull/29374)). Contributed by @gy-mate.
|
||||||
|
* Fix Docker Healthcheck ([#29471](https://github.com/element-hq/element-web/pull/29471)). Contributed by @benbz.
|
||||||
|
* Room List Store: Fetch rooms after space store is ready + attach store to window ([#29453](https://github.com/element-hq/element-web/pull/29453)). Contributed by @MidhunSureshR.
|
||||||
|
* Room List Store: Fix bug where left rooms appear in room list ([#29452](https://github.com/element-hq/element-web/pull/29452)). Contributed by @MidhunSureshR.
|
||||||
|
* Add space to the bottom of the room summary actions below leave room ([#29270](https://github.com/element-hq/element-web/pull/29270)). Contributed by @langleyd.
|
||||||
|
* Show error screens in group calls ([#29254](https://github.com/element-hq/element-web/pull/29254)). Contributed by @robintown.
|
||||||
|
* Prevent user from accidentally triggering multiple identity resets ([#29388](https://github.com/element-hq/element-web/pull/29388)). Contributed by @uhoreg.
|
||||||
|
* Remove buggy tooltip on room intro \& homepage ([#29406](https://github.com/element-hq/element-web/pull/29406)). Contributed by @t3chguy.
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
* [Backport staging] Log when we show, and hide, encryption setup toasts ([#29238](https://github.com/element-hq/element-web/pull/29238)). Contributed by @richvdh.
|
||||||
|
* Make profile header section match the designs ([#29163](https://github.com/element-hq/element-web/pull/29163)). Contributed by @MidhunSureshR.
|
||||||
|
* Always show back button in the right panel ([#29128](https://github.com/element-hq/element-web/pull/29128)). Contributed by @MidhunSureshR.
|
||||||
|
* Schedule dehydration on reload if the dehydration key is already cached locally ([#29021](https://github.com/element-hq/element-web/pull/29021)). Contributed by @uhoreg.
|
||||||
|
* update to twemoji 15.1.0 ([#29115](https://github.com/element-hq/element-web/pull/29115)). Contributed by @ara4n.
|
||||||
|
* Update matrix-widget-api ([#29112](https://github.com/element-hq/element-web/pull/29112)). Contributed by @toger5.
|
||||||
|
* Allow navigating through the memberlist using up/down keys ([#28949](https://github.com/element-hq/element-web/pull/28949)). Contributed by @MidhunSureshR.
|
||||||
|
* Style room header icons and facepile for toggled state ([#28968](https://github.com/element-hq/element-web/pull/28968)). Contributed by @MidhunSureshR.
|
||||||
|
* Move threads header below base card header ([#28969](https://github.com/element-hq/element-web/pull/28969)). Contributed by @MidhunSureshR.
|
||||||
|
* Add `Advanced` section to the user settings encryption tab ([#28804](https://github.com/element-hq/element-web/pull/28804)). Contributed by @florianduros.
|
||||||
|
* Fix outstanding UX issues with replies/mentions/keyword notifs ([#28270](https://github.com/element-hq/element-web/pull/28270)). Contributed by @taffyko.
|
||||||
|
* Distinguish room state and timeline events when dealing with widgets ([#28681](https://github.com/element-hq/element-web/pull/28681)). Contributed by @robintown.
|
||||||
|
* Switch OIDC primarily to new `/auth_metadata` API ([#29019](https://github.com/element-hq/element-web/pull/29019)). Contributed by @t3chguy.
|
||||||
|
* More memberlist changes ([#29069](https://github.com/element-hq/element-web/pull/29069)). Contributed by @MidhunSureshR.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* [Backport staging] Wire up the "Forgot recovery key" button for the "Key storage out of sync" toast ([#29190](https://github.com/element-hq/element-web/pull/29190)). Contributed by @RiotRobot.
|
||||||
|
* Encryption tab: hide `Advanced` section when the key storage is out of sync ([#29129](https://github.com/element-hq/element-web/pull/29129)). Contributed by @florianduros.
|
||||||
|
* Fix share button in discovery settings being disabled incorrectly ([#29151](https://github.com/element-hq/element-web/pull/29151)). Contributed by @t3chguy.
|
||||||
|
* Ensure switching rooms does not wrongly focus timeline search ([#29153](https://github.com/element-hq/element-web/pull/29153)). Contributed by @t3chguy.
|
||||||
|
* Stop showing a dialog prompting the user to enter an old recovery key ([#29143](https://github.com/element-hq/element-web/pull/29143)). Contributed by @richvdh.
|
||||||
|
* Make themed widgets reflect the effective theme ([#28342](https://github.com/element-hq/element-web/pull/28342)). Contributed by @robintown.
|
||||||
|
* support non-VS16 emoji ligatures in TwemojiMozilla ([#29100](https://github.com/element-hq/element-web/pull/29100)). Contributed by @ara4n.
|
||||||
|
* e2e test: Verify session with the encryption tab instead of the security \& privacy tab ([#29090](https://github.com/element-hq/element-web/pull/29090)). Contributed by @florianduros.
|
||||||
|
* Work around cloudflare R2 / aws client incompatability ([#29086](https://github.com/element-hq/element-web/pull/29086)). Contributed by @dbkr.
|
||||||
|
* Fix identity server settings visibility ([#29083](https://github.com/element-hq/element-web/pull/29083)). Contributed by @dbkr.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.91](https://github.com/element-hq/element-web/releases/tag/v1.11.91) (2025-01-28)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Implement changes to memberlist from feedback ([#29029](https://github.com/element-hq/element-web/pull/29029)). Contributed by @MidhunSureshR.
|
||||||
|
* Add toast for recovery keys being out of sync ([#28946](https://github.com/element-hq/element-web/pull/28946)). Contributed by @dbkr.
|
||||||
|
* Refactor LegacyCallHandler event emitter to use TypedEventEmitter ([#29008](https://github.com/element-hq/element-web/pull/29008)). Contributed by @t3chguy.
|
||||||
|
* Add `Recovery` section in the new user settings `Encryption` tab ([#28673](https://github.com/element-hq/element-web/pull/28673)). Contributed by @florianduros.
|
||||||
|
* Retry loading chunks to make the app more resilient ([#29001](https://github.com/element-hq/element-web/pull/29001)). Contributed by @t3chguy.
|
||||||
|
* Clear account idb table on logout ([#28996](https://github.com/element-hq/element-web/pull/28996)). Contributed by @t3chguy.
|
||||||
|
* Implement new memberlist design with MVVM architecture ([#28874](https://github.com/element-hq/element-web/pull/28874)). Contributed by @MidhunSureshR.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* [Backport staging] Switch to secure random strings ([#29035](https://github.com/element-hq/element-web/pull/29035)). Contributed by @RiotRobot.
|
||||||
|
* React to MatrixEvent sender/target being updated for rendering state events ([#28947](https://github.com/element-hq/element-web/pull/28947)). Contributed by @t3chguy.
|
||||||
|
|
||||||
|
|
||||||
|
Changes in [1.11.90](https://github.com/element-hq/element-web/releases/tag/v1.11.90) (2025-01-14)
|
||||||
|
==================================================================================================
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
* Docker: run as non-root ([#28849](https://github.com/element-hq/element-web/pull/28849)). Contributed by @richvdh.
|
||||||
|
* Docker: allow configuration of HTTP listen port via env var ([#28840](https://github.com/element-hq/element-web/pull/28840)). Contributed by @richvdh.
|
||||||
|
* Update matrix-wysiwyg to consume WASM asset ([#28838](https://github.com/element-hq/element-web/pull/28838)). Contributed by @t3chguy.
|
||||||
|
* OIDC settings tweaks ([#28787](https://github.com/element-hq/element-web/pull/28787)). Contributed by @t3chguy.
|
||||||
|
* Delabs native OIDC support ([#28615](https://github.com/element-hq/element-web/pull/28615)). Contributed by @t3chguy.
|
||||||
|
* Move room header info button to right-most position ([#28754](https://github.com/element-hq/element-web/pull/28754)). Contributed by @t3chguy.
|
||||||
|
* Enable key backup by default ([#28691](https://github.com/element-hq/element-web/pull/28691)). Contributed by @dbkr.
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
* Fix building the automations mermaid diagram ([#28881](https://github.com/element-hq/element-web/pull/28881)). Contributed by @dbkr.
|
||||||
|
* Playwright: wait for the network listener on the postgres db ([#28808](https://github.com/element-hq/element-web/pull/28808)). Contributed by @dbkr.
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18)
|
Changes in [1.11.89](https://github.com/element-hq/element-web/releases/tag/v1.11.89) (2024-12-18)
|
||||||
==================================================================================================
|
==================================================================================================
|
||||||
This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room.
|
This is a patch release to fix a bug which could prevent loading stored crypto state from storage, and also to fix URL previews when switching back to a room.
|
||||||
|
|||||||
@@ -189,89 +189,6 @@ give away to contributors - if you feel that Matrix-branded apparel is missing
|
|||||||
from your life, please mail us your shipping address to matrix at matrix.org
|
from your life, please mail us your shipping address to matrix at matrix.org
|
||||||
and we'll try to fix it :)
|
and we'll try to fix it :)
|
||||||
|
|
||||||
## Sign off
|
|
||||||
|
|
||||||
In order to have a concrete record that your contribution is intentional
|
|
||||||
and you agree to license it under the same terms as the project's license, we've
|
|
||||||
adopted the same lightweight approach that the Linux Kernel
|
|
||||||
(https://www.kernel.org/doc/html/latest/process/submitting-patches.html), Docker
|
|
||||||
(https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other
|
|
||||||
projects use: the DCO (Developer Certificate of Origin:
|
|
||||||
http://developercertificate.org/). This is a simple declaration that you wrote
|
|
||||||
the contribution or otherwise have the right to contribute it to Matrix:
|
|
||||||
|
|
||||||
```
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
If you agree to this for your contribution, then all that's needed is to
|
|
||||||
include the line in your commit or pull request comment:
|
|
||||||
|
|
||||||
```
|
|
||||||
Signed-off-by: Your Name <your@email.example.org>
|
|
||||||
```
|
|
||||||
|
|
||||||
We accept contributions under a legally identifiable name, such as your name on
|
|
||||||
government documentation or common-law names (names claimed by legitimate usage
|
|
||||||
or repute). Unfortunately, we cannot accept anonymous contributions at this
|
|
||||||
time.
|
|
||||||
|
|
||||||
Git allows you to add this signoff automatically when using the `-s` flag to
|
|
||||||
`git commit`, which uses the name and email set in your `user.name` and
|
|
||||||
`user.email` git configs.
|
|
||||||
|
|
||||||
If you forgot to sign off your commits before making your pull request and are
|
|
||||||
on Git 2.17+ you can mass signoff using rebase:
|
|
||||||
|
|
||||||
```
|
|
||||||
git rebase --signoff origin/develop
|
|
||||||
```
|
|
||||||
|
|
||||||
## Private sign off
|
|
||||||
|
|
||||||
If you would like to provide your legal name privately to the Matrix.org
|
|
||||||
Foundation (instead of in a public commit or comment), you can do so by emailing
|
|
||||||
your legal name and a link to the pull request to dco@matrix.org. It helps to
|
|
||||||
include "sign off" or similar in the subject line. You will then be instructed
|
|
||||||
further.
|
|
||||||
|
|
||||||
Once private sign off is complete, doing so for future contributions will not
|
|
||||||
be required.
|
|
||||||
|
|
||||||
# Review expectations
|
# Review expectations
|
||||||
|
|
||||||
See https://github.com/element-hq/element-meta/wiki/Review-process
|
See https://github.com/element-hq/element-meta/wiki/Review-process
|
||||||
|
|||||||
24
Dockerfile
24
Dockerfile
@@ -1,5 +1,7 @@
|
|||||||
|
# syntax=docker.io/docker/dockerfile:1.15-labs@sha256:94edd5b349df43675bd6f542e2b9a24e7177432dec45fe3066bfcf2ab14c4355
|
||||||
|
|
||||||
# Builder
|
# Builder
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS builder
|
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:ed0338dd02fd86861a59dc1cbc2e12152f3a93c4ce5933d347d6677232000dc7 AS builder
|
||||||
|
|
||||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||||
ARG USE_CUSTOM_SDKS=false
|
ARG USE_CUSTOM_SDKS=false
|
||||||
@@ -8,7 +10,7 @@ ARG JS_SDK_BRANCH="master"
|
|||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
COPY . /src
|
COPY --exclude=docker . /src
|
||||||
RUN /src/scripts/docker-link-repos.sh
|
RUN /src/scripts/docker-link-repos.sh
|
||||||
RUN yarn --network-timeout=200000 install
|
RUN yarn --network-timeout=200000 install
|
||||||
RUN /src/scripts/docker-package.sh
|
RUN /src/scripts/docker-package.sh
|
||||||
@@ -17,20 +19,20 @@ RUN /src/scripts/docker-package.sh
|
|||||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||||
|
|
||||||
# App
|
# App
|
||||||
FROM nginx:alpine-slim
|
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:0a49675e3e35cc2f89ce831f00f767af9c32df04f5a80167739fd32346f1fe99
|
||||||
|
|
||||||
|
# Need root user to install packages & manipulate the usr directory
|
||||||
|
USER root
|
||||||
|
|
||||||
|
# Install jq and moreutils for sponge, both used by our entrypoints
|
||||||
|
RUN apk add jq moreutils
|
||||||
|
|
||||||
COPY --from=builder /src/webapp /app
|
COPY --from=builder /src/webapp /app
|
||||||
|
|
||||||
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
# Override default nginx config. Templates in `/etc/nginx/templates` are passed
|
||||||
# through `envsubst` by the nginx docker image entry point.
|
# through `envsubst` by the nginx docker image entry point.
|
||||||
COPY /docker/nginx-templates/* /etc/nginx/templates/
|
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
|
|
||||||
|
|
||||||
# nginx user must own the cache and etc directory to write cache and tweak the nginx config
|
|
||||||
RUN chown -R nginx:0 /var/cache/nginx /etc/nginx
|
|
||||||
RUN chmod -R g+w /var/cache/nginx /etc/nginx
|
|
||||||
|
|
||||||
RUN rm -rf /usr/share/nginx/html \
|
RUN rm -rf /usr/share/nginx/html \
|
||||||
&& ln -s /app /usr/share/nginx/html
|
&& ln -s /app /usr/share/nginx/html
|
||||||
@@ -40,3 +42,5 @@ USER nginx
|
|||||||
|
|
||||||
# HTTP listen port
|
# HTTP listen port
|
||||||
ENV ELEMENT_WEB_PORT=80
|
ENV ELEMENT_WEB_PORT=80
|
||||||
|
|
||||||
|
HEALTHCHECK --start-period=5s CMD wget -q --spider http://localhost:$ELEMENT_WEB_PORT/config.json
|
||||||
|
|||||||
122
README.md
122
README.md
@@ -126,7 +126,7 @@ guide](https://classic.yarnpkg.com/en/docs/install) if you do not have it alread
|
|||||||
1. Install the prerequisites: `yarn install`.
|
1. Install the prerequisites: `yarn install`.
|
||||||
- If you're using the `develop` branch, then it is recommended to set up a
|
- If you're using the `develop` branch, then it is recommended to set up a
|
||||||
proper development environment (see [Setting up a dev
|
proper development environment (see [Setting up a dev
|
||||||
environment](#setting-up-a-dev-environment) below). Alternatively, you
|
environment](./developer_guide.md#setting-up-a-dev-environment) below). Alternatively, you
|
||||||
can use <https://develop.element.io> - the continuous integration release of
|
can use <https://develop.element.io> - the continuous integration release of
|
||||||
the develop branch.
|
the develop branch.
|
||||||
1. Configure the app by copying `config.sample.json` to `config.json` and
|
1. Configure the app by copying `config.sample.json` to `config.json` and
|
||||||
@@ -182,123 +182,11 @@ Dockerfile.
|
|||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
Before attempting to develop on Element you **must** read the [developer guide
|
Please read through the following:
|
||||||
for `matrix-react-sdk`](https://github.com/matrix-org/matrix-react-sdk#developer-guide), which
|
|
||||||
also defines the design, architecture and style for Element too.
|
|
||||||
|
|
||||||
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
1. [Developer guide](./developer_guide.md)
|
||||||
about where to start. Before starting work on a feature, it's best to ensure
|
2. [Code style](./code_style.md)
|
||||||
your plan aligns well with our vision for Element. Please chat with the team in
|
3. [Contribution guide](./CONTRIBUTING.md)
|
||||||
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
|
||||||
you start so we can ensure it's something we'd be willing to merge.
|
|
||||||
|
|
||||||
You should also familiarise yourself with the ["Here be Dragons" guide
|
|
||||||
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
|
||||||
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
|
||||||
|
|
||||||
The idea of Element is to be a relatively lightweight "skin" of customisations on
|
|
||||||
top of the underlying `matrix-react-sdk`. `matrix-react-sdk` provides both the
|
|
||||||
higher and lower level React components useful for building Matrix communication
|
|
||||||
apps using React.
|
|
||||||
|
|
||||||
Please note that Element is intended to run correctly without access to the public
|
|
||||||
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
|
||||||
hosted by external CDNs or servers but instead please package all dependencies
|
|
||||||
into Element itself.
|
|
||||||
|
|
||||||
# Setting up a dev environment
|
|
||||||
|
|
||||||
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
|
||||||
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
|
||||||
in git and to make local changes without having to manually rebuild each time.
|
|
||||||
|
|
||||||
First clone and build `matrix-js-sdk`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
|
||||||
pushd matrix-js-sdk
|
|
||||||
yarn link
|
|
||||||
yarn install
|
|
||||||
popd
|
|
||||||
```
|
|
||||||
|
|
||||||
Clone the repo and switch to the `element-web` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/element-hq/element-web.git
|
|
||||||
cd element-web
|
|
||||||
```
|
|
||||||
|
|
||||||
Configure the app by copying `config.sample.json` to `config.json` and
|
|
||||||
modifying it. See the [configuration docs](docs/config.md) for details.
|
|
||||||
|
|
||||||
Finally, build and start Element itself:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn link matrix-js-sdk
|
|
||||||
yarn install
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
Wait a few seconds for the initial build to finish; you should see something like:
|
|
||||||
|
|
||||||
```
|
|
||||||
[element-js] <s> [webpack.Progress] 100%
|
|
||||||
[element-js]
|
|
||||||
[element-js] ℹ 「wdm」: 1840 modules
|
|
||||||
[element-js] ℹ 「wdm」: Compiled successfully.
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember, the command will not terminate since it runs the web server
|
|
||||||
and rebuilds source files when they change. This development server also
|
|
||||||
disables caching, so do NOT use it in production.
|
|
||||||
|
|
||||||
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
|
||||||
|
|
||||||
**Note**: The build script uses inotify by default on Linux to monitor directories
|
|
||||||
for changes. If the inotify limits are too low your build will fail silently or with
|
|
||||||
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
|
||||||
of at least `128M` and instance limit around `512`.
|
|
||||||
|
|
||||||
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
|
||||||
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
|
||||||
|
|
||||||
To set a new inotify watch and instance limit, execute:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo sysctl fs.inotify.max_user_watches=131072
|
|
||||||
sudo sysctl fs.inotify.max_user_instances=512
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wish, you can make the new limits permanent, by executing:
|
|
||||||
|
|
||||||
```
|
|
||||||
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
|
||||||
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
|
||||||
sudo sysctl -p
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
|
||||||
|
|
||||||
If any of these steps error with, `file table overflow`, you are probably on a mac
|
|
||||||
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
|
||||||
You'll need to do this in each new terminal you open before building Element.
|
|
||||||
|
|
||||||
## Running the tests
|
|
||||||
|
|
||||||
There are a number of application-level tests in the `tests` directory; these
|
|
||||||
are designed to run with Jest and JSDOM. To run them
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn test
|
|
||||||
```
|
|
||||||
|
|
||||||
### End-to-End tests
|
|
||||||
|
|
||||||
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class MockMap extends EventEmitter {
|
|||||||
setCenter = jest.fn();
|
setCenter = jest.fn();
|
||||||
setStyle = jest.fn();
|
setStyle = jest.fn();
|
||||||
fitBounds = jest.fn();
|
fitBounds = jest.fn();
|
||||||
|
remove = jest.fn();
|
||||||
}
|
}
|
||||||
const MockMapInstance = new MockMap();
|
const MockMapInstance = new MockMap();
|
||||||
|
|
||||||
|
|||||||
@@ -31,5 +31,7 @@ module.exports = {
|
|||||||
|
|
||||||
"@babel/plugin-syntax-dynamic-import",
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
"@babel/plugin-transform-runtime",
|
"@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
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,15 +5,6 @@ adjacent to. As of writing, these are:
|
|||||||
|
|
||||||
- element-desktop
|
- element-desktop
|
||||||
- element-web
|
- element-web
|
||||||
- matrix-js-sdk
|
|
||||||
|
|
||||||
Other projects might extend this code style for increased strictness. For example, matrix-events-sdk
|
|
||||||
has stricter code organization to reduce the maintenance burden. These projects will declare their code
|
|
||||||
style within their own repos.
|
|
||||||
|
|
||||||
Note that some requirements will be layer-specific. Where the requirements don't make sense for the
|
|
||||||
project, they are used to the best of their ability, used in spirit, or ignored if not applicable,
|
|
||||||
in that order.
|
|
||||||
|
|
||||||
## Guiding principles
|
## Guiding principles
|
||||||
|
|
||||||
@@ -234,17 +225,19 @@ Unless otherwise specified, the following applies to all code:
|
|||||||
|
|
||||||
Inheriting all the rules of TypeScript, the following additionally apply:
|
Inheriting all the rules of TypeScript, the following additionally apply:
|
||||||
|
|
||||||
1. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
1. Component source files are named with upper camel case (e.g. views/rooms/EventTile.js)
|
||||||
2. Class components must always have a `Props` interface declared immediately above them. It can be
|
2. They are organised in a typically two-level hierarchy - first whether the component is a view or a structure, and then a broad functional grouping (e.g. 'rooms' here)
|
||||||
|
3. Types for lifecycle functions are not required (render, componentDidMount, and so on).
|
||||||
|
4. Class components must always have a `Props` interface declared immediately above them. It can be
|
||||||
empty if the component accepts no props.
|
empty if the component accepts no props.
|
||||||
3. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
5. Class components should have an `State` interface declared immediately above them, but after `Props`.
|
||||||
4. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
6. Props and State should not be exported. Use `React.ComponentProps<typeof ComponentNameHere>`
|
||||||
instead.
|
instead.
|
||||||
5. One component per file, except when a component is a utility component specifically for the "primary"
|
7. One component per file, except when a component is a utility component specifically for the "primary"
|
||||||
component. The utility component should not be exported.
|
component. The utility component should not be exported.
|
||||||
6. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
8. Exported constants, enums, interfaces, functions, etc must be separate from files containing components
|
||||||
or stores.
|
or stores.
|
||||||
7. Stores should use a singleton pattern with a static instance property:
|
9. Stores should use a singleton pattern with a static instance property:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class FooStore {
|
class FooStore {
|
||||||
@@ -261,44 +254,41 @@ Inheriting all the rules of TypeScript, the following additionally apply:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Stores must support using an alternative MatrixClient and dispatcher instance.
|
10. Stores must support using an alternative MatrixClient and dispatcher instance.
|
||||||
9. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
11. Utilities which require JSX must be split out from utilities which do not. This is to prevent import
|
||||||
cycles during runtime where components accidentally include more of the app than they intended.
|
cycles during runtime where components accidentally include more of the app than they intended.
|
||||||
10. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
12. Interdependence between stores should be kept to a minimum. Break functions and constants out to utilities
|
||||||
if at all possible.
|
if at all possible.
|
||||||
11. A component should only use CSS class names in line with the component name.
|
13. A component should only use CSS class names in line with the component name.
|
||||||
|
|
||||||
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
1. When knowingly using a class name from another component, document it with a [comment](#comments).
|
||||||
|
|
||||||
12. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
14. Curly braces within JSX should be padded with a space, however properties on those components should not.
|
||||||
See above code example.
|
See above code example.
|
||||||
13. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
15. Functions used as properties should either be defined on the class or stored in a variable. They should not
|
||||||
be inline unless mocking/short-circuiting the value.
|
be inline unless mocking/short-circuiting the value.
|
||||||
14. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
16. Prefer hooks (functional components) over class components. Be consistent with the existing area if unsure
|
||||||
which should be used.
|
which should be used.
|
||||||
1. Unless the component is considered a "structure", in which case use classes.
|
1. Unless the component is considered a "structure", in which case use classes.
|
||||||
15. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
17. Write more views than structures. Structures are chunks of functionality like MatrixChat while views are
|
||||||
isolated components.
|
isolated components.
|
||||||
16. Components should serve a single, or near-single, purpose.
|
18. Components should serve a single, or near-single, purpose.
|
||||||
17. Prefer to derive information from component properties rather than establish state.
|
19. Prefer to derive information from component properties rather than establish state.
|
||||||
18. Do not use `React.Component::forceUpdate`.
|
20. Do not use `React.Component::forceUpdate`.
|
||||||
|
|
||||||
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
## Stylesheets (\*.pcss = PostCSS + Plugins)
|
||||||
|
|
||||||
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
|
Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, but actually it is not.
|
||||||
|
|
||||||
1. Class names must be prefixed with "mx\_".
|
1. The view's CSS file MUST have the same name as the component (e.g. `view/rooms/_MessageTile.css` for `MessageTile.tsx` component).
|
||||||
2. Class names must denote the component which defines them, followed by any context.
|
2. Per-view CSS is optional - it could choose to inherit all its styling from the context of the rest of the app, although this is unusual.
|
||||||
The context is not further specified here in terms of meaning or syntax.
|
3. Class names must be prefixed with "mx\_".
|
||||||
Use whatever is appropriate for your implementation use case.
|
4. Class names must strictly denote the component which defines them.
|
||||||
Some examples:
|
For example: `mx_MyFoo` for `MyFoo` component.
|
||||||
1. `mx_MyFoo`
|
5. Class names for DOM elements within a view which aren't components are named by appending a lower camel case identifier to the view's class name - e.g. .mx_MyFoo_randomDiv is how you'd name the class of an arbitrary div within the MyFoo view.
|
||||||
2. `mx_MyFoo_avatar`
|
6. Use the `$font` variables instead of manual values.
|
||||||
3. `mx_MyFoo_avatarUser`
|
7. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
||||||
4. `mx_MyFoo_avatar--user`
|
8. Use the whole class name instead of shortcuts:
|
||||||
3. Use the `$font` variables instead of manual values.
|
|
||||||
4. Keep indentation/nesting to a minimum. Maximum suggested nesting is 5 layers.
|
|
||||||
5. Use the whole class name instead of shortcuts:
|
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo {
|
.mx_MyFoo {
|
||||||
@@ -309,7 +299,7 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Break multiple selectors over multiple lines this way:
|
9. Break multiple selectors over multiple lines this way:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
.mx_MyFoo,
|
.mx_MyFoo,
|
||||||
@@ -319,8 +309,8 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
10. Non-shared variables should use $lowerCamelCase. Shared variables use $dashed-naming.
|
||||||
8. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
11. Overrides to Z indexes, adjustments of dimensions/padding with pixels, and so on should all be
|
||||||
[documented](#comments) for what the values mean:
|
[documented](#comments) for what the values mean:
|
||||||
|
|
||||||
```scss
|
```scss
|
||||||
@@ -331,7 +321,9 @@ Note: We use PostCSS + some plugins to process our styles. It looks like SCSS, b
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
9. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
12. Avoid the use of `!important`. If `!important` is necessary, add a [comment](#comments) explaining why.
|
||||||
|
13. The CSS for a component can override the rules for child components. For instance, .mxRoomList .mx_RoomTile {} would be the selector to override styles of RoomTiles when viewed in the context of a RoomList view. Overrides must be scoped to the View's CSS class - i.e. don't just define .mx_RoomTile {} in RoomList.css - only RoomTile.css is allowed to define its own CSS. Instead, say .mx_RoomList .mx_RoomTile {} to scope the override only to the context of RoomList views. N.B. overrides should be relatively rare as in general CSS inheritance should be enough.
|
||||||
|
14. Components should render only within the bounding box of their outermost DOM element. Page-absolute positioning and negative CSS margins and similar are generally not cool and stop the component from being reused easily in different places.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Element",
|
|
||||||
"description": "A glossy Matrix collaboration client for the web.",
|
|
||||||
"repository": {
|
|
||||||
"url": "https://github.com/element-hq/element-web",
|
|
||||||
"license": "AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"list": "https://github.com/element-hq/element-web/issues",
|
|
||||||
"report": "https://github.com/element-hq/element-web/issues/new/choose"
|
|
||||||
},
|
|
||||||
"keywords": ["chat", "riot", "matrix"]
|
|
||||||
}
|
|
||||||
2
debian/control
vendored
2
debian/control
vendored
@@ -8,6 +8,6 @@ Package: element-web
|
|||||||
Architecture: all
|
Architecture: all
|
||||||
Recommends: httpd, element-io-archive-keyring
|
Recommends: httpd, element-io-archive-keyring
|
||||||
Description:
|
Description:
|
||||||
A feature-rich client for Matrix.org
|
Element: the future of secure communication
|
||||||
This package contains the web-based client that can be served through a web
|
This package contains the web-based client that can be served through a web
|
||||||
server.
|
server.
|
||||||
|
|||||||
126
developer_guide.md
Normal file
126
developer_guide.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# Developer Guide
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Read the [Choosing an issue](docs/choosing-an-issue.md) page for some guidance
|
||||||
|
about where to start. Before starting work on a feature, it's best to ensure
|
||||||
|
your plan aligns well with our vision for Element. Please chat with the team in
|
||||||
|
[#element-dev:matrix.org](https://matrix.to/#/#element-dev:matrix.org) before
|
||||||
|
you start so we can ensure it's something we'd be willing to merge.
|
||||||
|
|
||||||
|
You should also familiarise yourself with the ["Here be Dragons" guide
|
||||||
|
](https://docs.google.com/document/d/12jYzvkidrp1h7liEuLIe6BMdU0NUjndUYI971O06ooM)
|
||||||
|
to the tame & not-so-tame dragons (gotchas) which exist in the codebase.
|
||||||
|
|
||||||
|
Please note that Element is intended to run correctly without access to the public
|
||||||
|
internet. So please don't depend on resources (JS libs, CSS, images, fonts)
|
||||||
|
hosted by external CDNs or servers but instead please package all dependencies
|
||||||
|
into Element itself.
|
||||||
|
|
||||||
|
## Setting up a dev environment
|
||||||
|
|
||||||
|
Much of the functionality in Element is actually in the `matrix-js-sdk` module.
|
||||||
|
It is possible to set these up in a way that makes it easy to track the `develop` branches
|
||||||
|
in git and to make local changes without having to manually rebuild each time.
|
||||||
|
|
||||||
|
First clone and build `matrix-js-sdk`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/matrix-org/matrix-js-sdk.git
|
||||||
|
pushd matrix-js-sdk
|
||||||
|
yarn link
|
||||||
|
yarn install
|
||||||
|
popd
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the repo and switch to the `element-web` directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/element-hq/element-web.git
|
||||||
|
cd element-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the app by copying `config.sample.json` to `config.json` and
|
||||||
|
modifying it. See the [configuration docs](docs/config.md) for details.
|
||||||
|
|
||||||
|
Finally, build and start Element itself:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn link matrix-js-sdk
|
||||||
|
yarn install
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait a few seconds for the initial build to finish; you should see something like:
|
||||||
|
|
||||||
|
```
|
||||||
|
[element-js] <s> [webpack.Progress] 100%
|
||||||
|
[element-js]
|
||||||
|
[element-js] ℹ 「wdm」: 1840 modules
|
||||||
|
[element-js] ℹ 「wdm」: Compiled successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember, the command will not terminate since it runs the web server
|
||||||
|
and rebuilds source files when they change. This development server also
|
||||||
|
disables caching, so do NOT use it in production.
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:8080/> in your browser to see your newly built Element.
|
||||||
|
|
||||||
|
**Note**: The build script uses inotify by default on Linux to monitor directories
|
||||||
|
for changes. If the inotify limits are too low your build will fail silently or with
|
||||||
|
`Error: EMFILE: too many open files`. To avoid these issues, we recommend a watch limit
|
||||||
|
of at least `128M` and instance limit around `512`.
|
||||||
|
|
||||||
|
You may be interested in issues [#15750](https://github.com/element-hq/element-web/issues/15750) and
|
||||||
|
[#15774](https://github.com/element-hq/element-web/issues/15774) for further details.
|
||||||
|
|
||||||
|
To set a new inotify watch and instance limit, execute:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo sysctl fs.inotify.max_user_watches=131072
|
||||||
|
sudo sysctl fs.inotify.max_user_instances=512
|
||||||
|
sudo sysctl -p
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish, you can make the new limits permanent, by executing:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo fs.inotify.max_user_watches=131072 | sudo tee -a /etc/sysctl.conf
|
||||||
|
echo fs.inotify.max_user_instances=512 | sudo tee -a /etc/sysctl.conf
|
||||||
|
sudo sysctl -p
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
When you make changes to `matrix-js-sdk` they should be automatically picked up by webpack and built.
|
||||||
|
|
||||||
|
If any of these steps error with, `file table overflow`, you are probably on a mac
|
||||||
|
which has a very low limit on max open files. Run `ulimit -Sn 1024` and try again.
|
||||||
|
You'll need to do this in each new terminal you open before building Element.
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
There are a number of application-level tests in the `tests` directory; these
|
||||||
|
are designed to run with Jest and JSDOM. To run them
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
### End-to-End tests
|
||||||
|
|
||||||
|
See [matrix-react-sdk](https://github.com/matrix-org/matrix-react-sdk/#end-to-end-tests) for how to run the end-to-end tests.
|
||||||
|
|
||||||
|
## General github guidelines
|
||||||
|
|
||||||
|
1. **Pull requests must only be filed against the `develop` branch.**
|
||||||
|
2. Try to keep your pull requests concise. Split them up if necessary.
|
||||||
|
3. Ensure that you provide a description that explains the fix/feature and its intent.
|
||||||
|
|
||||||
|
## Adding new code
|
||||||
|
|
||||||
|
New code should be committed as follows:
|
||||||
|
|
||||||
|
- All new components: https://github.com/element-hq/element-web/tree/develop/src/components
|
||||||
|
- CSS: https://github.com/element-hq/element-web/tree/develop/res/css
|
||||||
|
- Theme specific CSS & resources: https://github.com/element-hq/element-web/tree/develop/res/themes
|
||||||
34
docker/docker-entrypoint.d/18-load-element-modules.sh
Executable file
34
docker/docker-entrypoint.d/18-load-element-modules.sh
Executable 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
|
||||||
@@ -18,8 +18,12 @@ server {
|
|||||||
}
|
}
|
||||||
# covers config.json and config.hostname.json requests as it is prefix.
|
# covers config.json and config.hostname.json requests as it is prefix.
|
||||||
location /config {
|
location /config {
|
||||||
|
root /tmp/element-web-config;
|
||||||
add_header Cache-Control "no-cache";
|
add_header Cache-Control "no-cache";
|
||||||
}
|
}
|
||||||
|
location /modules {
|
||||||
|
alias /modules;
|
||||||
|
}
|
||||||
# redirect server error pages to the static page /50x.html
|
# redirect server error pages to the static page /50x.html
|
||||||
#
|
#
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
67
docs/MVVM.md
Normal file
67
docs/MVVM.md
Normal 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).
|
||||||
@@ -46,7 +46,6 @@
|
|||||||
- [Skinning](skinning.md)
|
- [Skinning](skinning.md)
|
||||||
- [Cider editor](ciderEditor.md)
|
- [Cider editor](ciderEditor.md)
|
||||||
- [Iconography](icons.md)
|
- [Iconography](icons.md)
|
||||||
- [Jitsi](jitsi.md)
|
|
||||||
- [Local echo](local-echo-dev.md)
|
- [Local echo](local-echo-dev.md)
|
||||||
- [Media](media-handling.md)
|
- [Media](media-handling.md)
|
||||||
- [Room List Store](room-list-store.md)
|
- [Room List Store](room-list-store.md)
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ complete re-branding/private labeling, a more personalised experience can be ach
|
|||||||
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
3. `show_once`: Optional. If true then the notice will only be shown once per device.
|
||||||
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
18. `help_url`: The URL to point users to for help with the app, defaults to `https://element.io/help`.
|
||||||
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
19. `help_encryption_url`: The URL to point users to for help with encryption, defaults to `https://element.io/help#encryption`.
|
||||||
20. `force_verification`: If true, users must verify new logins (eg. with another device / their security key)
|
20. `force_verification`: If true, users must verify new logins (eg. with another device / their recovery key)
|
||||||
|
|
||||||
### `desktop_builds` and `mobile_builds`
|
### `desktop_builds` and `mobile_builds`
|
||||||
|
|
||||||
@@ -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
|
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.
|
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.
|
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.
|
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.
|
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.
|
4. `url_macos`: Optional. Direct link to download macOS desktop app.
|
||||||
5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app.
|
5. `url_win64`: Optional. Direct link to download Windows x86 64-bit desktop app.
|
||||||
6. `url_win64`: Optional. Direct link to download Windows 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.
|
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
|
When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io
|
||||||
@@ -384,8 +384,6 @@ The VoIP and Jitsi options are:
|
|||||||
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
5. `audio_stream_url`: Optional URL to pass to Jitsi to enable live streaming. This option is considered experimental and may be removed
|
||||||
at any time without notice.
|
at any time without notice.
|
||||||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||||
- `url`: The URL of the Element Call instance to use for native group calls. This option is considered experimental
|
|
||||||
and may be removed at any time without notice. Defaults to `https://call.element.io`.
|
|
||||||
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
- `use_exclusively`: A boolean specifying whether Element Call should be used exclusively as the only VoIP stack in
|
||||||
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
the app, removing the ability to start legacy 1:1 calls or Jitsi calls. Defaults to `false`.
|
||||||
- `participant_limit`: The maximum number of users who can join a call; if
|
- `participant_limit`: The maximum number of users who can join a call; if
|
||||||
@@ -592,3 +590,4 @@ The following are undocumented or intended for developer use only.
|
|||||||
2. `sync_timeline_limit`
|
2. `sync_timeline_limit`
|
||||||
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
3. `dangerously_allow_unsafe_and_insecure_passwords`
|
||||||
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
|
||||||
|
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
||||||
|
|||||||
@@ -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
|
image as root (`docker run --user 0`) or, better, change the port that nginx
|
||||||
listens on via the `ELEMENT_WEB_PORT` environment variable.
|
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
|
The behaviour of the docker image can be customised via the following
|
||||||
environment variables:
|
environment variables:
|
||||||
|
|
||||||
|
|||||||
@@ -101,10 +101,6 @@ Under the hood this stops Element Web from adding the `perParticipantE2EE` flag
|
|||||||
|
|
||||||
This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.
|
This is useful while we experiment with encryption and to make calling compatible with platforms that don't use encryption yet.
|
||||||
|
|
||||||
## Rich text in room topics (`feature_html_topic`) [In Development]
|
|
||||||
|
|
||||||
Enables rendering of MD / HTML in room topics.
|
|
||||||
|
|
||||||
## Enable the notifications panel in the room header (`feature_notifications`)
|
## Enable the notifications panel in the room header (`feature_notifications`)
|
||||||
|
|
||||||
Unreliable in encrypted rooms.
|
Unreliable in encrypted rooms.
|
||||||
@@ -112,3 +108,7 @@ Unreliable in encrypted rooms.
|
|||||||
## Knock rooms (`feature_ask_to_join`) [In Development]
|
## Knock rooms (`feature_ask_to_join`) [In Development]
|
||||||
|
|
||||||
Enables knock feature for rooms. This allows users to ask to join a room.
|
Enables knock feature for rooms. This allows users to ask to join a room.
|
||||||
|
|
||||||
|
## New room list (`feature_new_room_list`) [In Development]
|
||||||
|
|
||||||
|
Enable the new room list that is currently in development.
|
||||||
|
|||||||
@@ -23,21 +23,19 @@ element-web project is fine: leave it running it a different terminal as you wou
|
|||||||
when developing. Alternatively if you followed the development set up from element-web then
|
when developing. Alternatively if you followed the development set up from element-web then
|
||||||
Playwright will be capable of running the webserver on its own if it isn't already running.
|
Playwright will be capable of running the webserver on its own if it isn't already running.
|
||||||
|
|
||||||
The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also
|
The tests use [testcontainers](https://node.testcontainers.org/) to launch Homeserver (Synapse or Dendrite)
|
||||||
need to have Docker installed and working in order to run the Playwright tests.
|
instances to test against, so you'll also need to one of the
|
||||||
|
[supported container runtimes](#supporter-container-runtimes)
|
||||||
|
installed and working in order to run the Playwright tests.
|
||||||
|
|
||||||
There are a few different ways to run the tests yourself. The simplest is to run:
|
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker pull ghcr.io/element-hq/synapse:develop
|
|
||||||
yarn run test:playwright
|
yarn run test:playwright
|
||||||
```
|
```
|
||||||
|
|
||||||
This will run the Playwright tests once, non-interactively.
|
This will run the Playwright tests once, non-interactively.
|
||||||
|
|
||||||
Note: you don't need to run the `docker pull` command every time, but you should
|
|
||||||
do it regularly to ensure you are running against an up-to-date Synapse.
|
|
||||||
|
|
||||||
You can also run individual tests this way too, as you'd expect:
|
You can also run individual tests this way too, as you'd expect:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
@@ -61,29 +59,28 @@ Some tests are excluded from running on certain browsers due to incompatibilitie
|
|||||||
|
|
||||||
## How the Tests Work
|
## How the Tests Work
|
||||||
|
|
||||||
Everything Playwright-related lives in the `playwright/` subdirectory of react-sdk
|
Everything Playwright-related lives in the `playwright/` subdirectory
|
||||||
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||||
|
|
||||||
`playwright/plugins/homeservers` contains Playwright plugins that starts instances
|
`playwright/testcontainers` contains the testcontainers which start instances
|
||||||
of Synapse/Dendrite in Docker containers. These servers are what Element-web runs
|
of Synapse/Dendrite. These servers are what Element-web runs against in the tests.
|
||||||
against in the tests.
|
|
||||||
|
|
||||||
Synapse can be launched with different configurations in order to test element
|
Synapse can be launched with different configurations in order to test element
|
||||||
in different configurations. `playwright/plugins/homeserver/synapse/templates`
|
in different configurations. You can specify `synapseConfig` as such:
|
||||||
contains template configuration files for each different configuration.
|
|
||||||
|
|
||||||
Each test suite can then launch whatever Synapse instances it needs in whatever
|
```typescript
|
||||||
configurations.
|
test.use({
|
||||||
|
synapseConfig: {
|
||||||
|
// The config options to pass to the Synapse instance
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
Note that although tests should stop the Homeserver instances after running and the
|
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
|
||||||
plugin also stop any remaining instances after all tests have run, it is possible
|
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
|
||||||
to be left with some stray containers if, for example, you terminate a test such
|
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||||
that the `after()` does not run and also exit Playwright uncleanly. All the containers
|
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||||
it starts are prefixed, so they are easy to recognise. They can be removed safely.
|
The logs from testcontainers will be attached to any reports output from Playwright.
|
||||||
|
|
||||||
After each test run, logs from the Synapse instances are saved in `playwright/logs/synapse`
|
|
||||||
with each instance in a separate directory named after its ID. These logs are removed
|
|
||||||
at the start of each test run.
|
|
||||||
|
|
||||||
## Writing Tests
|
## Writing Tests
|
||||||
|
|
||||||
@@ -113,25 +110,6 @@ Homeserver instances should be reasonably cheap to start (you may see the first
|
|||||||
while as it pulls the Docker image).
|
while as it pulls the Docker image).
|
||||||
You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.
|
You do not need to explicitly clean up the instance as it will be cleaned up by the fixture.
|
||||||
|
|
||||||
### Synapse Config Templates
|
|
||||||
|
|
||||||
When a Synapse instance is started, it's given a config generated from one of the config
|
|
||||||
templates in `playwright/plugins/homeserver/synapse/templates`. There are a couple of special files
|
|
||||||
in these templates:
|
|
||||||
|
|
||||||
- `homeserver.yaml`:
|
|
||||||
Template substitution happens in this file. Template variables are:
|
|
||||||
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
|
|
||||||
- `MACAROON_SECRET_KEY`: Generated each time for security
|
|
||||||
- `FORM_SECRET`: Generated each time for security
|
|
||||||
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
|
|
||||||
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
|
|
||||||
Config templates should not contain a signing key and instead assume that one will exist
|
|
||||||
in this file.
|
|
||||||
|
|
||||||
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
|
|
||||||
in a template can be referenced in the config as `/data/foo.html`.
|
|
||||||
|
|
||||||
### Logging In
|
### Logging In
|
||||||
|
|
||||||
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||||
@@ -227,7 +205,13 @@ has to be disabled in Playwright on Firefox & Webkit to retain routing functiona
|
|||||||
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
||||||
there at this time.
|
there at this time.
|
||||||
|
|
||||||
## Colima
|
If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`.
|
||||||
|
|
||||||
|
## Supporter container runtimes
|
||||||
|
|
||||||
|
We use testcontainers to spin up various instances of Synapse, Matrix Authentication Service, and more.
|
||||||
|
It supports Docker out of the box but also has support for Podman, Colima, Rancher, you just need to follow some instructions to achieve it:
|
||||||
|
https://node.testcontainers.org/supported-container-runtimes/
|
||||||
|
|
||||||
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
|
If you are running under Colima, you may need to set the environment variable `TMPDIR` to `/tmp/colima` or a path
|
||||||
within `$HOME` to allow bind mounting temporary directories into the Docker containers.
|
within `$HOME` to allow bind mounting temporary directories into the Docker containers.
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
|
|
||||||
#### develop
|
#### develop
|
||||||
|
|
||||||
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable. It corresponds to the develop.element.io CD platform.
|
The develop branch holds the very latest and greatest code we have to offer, as such it may be less stable.
|
||||||
|
It is auto-deployed on every commit to element-web or matrix-js-sdk to develop.element.io via GitHub Actions `build_develop.yml`.
|
||||||
|
|
||||||
#### staging
|
#### staging
|
||||||
|
|
||||||
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
|
The staging branch corresponds to the very latest release regardless of whether it is an RC or not. Deployed to staging.element.io manually.
|
||||||
|
It is auto-deployed on every release of element-web to staging.element.io via GitHub Actions `deploy.yml`.
|
||||||
|
|
||||||
#### master
|
#### master
|
||||||
|
|
||||||
@@ -126,7 +128,7 @@ flowchart TD
|
|||||||
|
|
||||||
subgraph Deploying
|
subgraph Deploying
|
||||||
D1[\Deploy staging.element.io/]
|
D1[\Deploy staging.element.io/]
|
||||||
D2[\Check dockerhub/]
|
D2[\Check docker build/]
|
||||||
D3[\Deploy app.element.io/]
|
D3[\Deploy app.element.io/]
|
||||||
D4[\Check desktop package/]
|
D4[\Check desktop package/]
|
||||||
|
|
||||||
@@ -211,11 +213,11 @@ switched back to the version of the dependency from the master branch to not lea
|
|||||||
# Deploying
|
# Deploying
|
||||||
|
|
||||||
We ship the SDKs to npm, this happens as part of the release process.
|
We ship the SDKs to npm, this happens as part of the release process.
|
||||||
We ship Element Web to dockerhub, `*.element.io`, and packages.element.io.
|
We ship Element Web to dockerhub, ghcr.io, `*.element.io`, and packages.element.io.
|
||||||
We ship Element Desktop to packages.element.io.
|
We ship Element Desktop to packages.element.io.
|
||||||
|
|
||||||
- [ ] Check that element-web has shipped to dockerhub
|
- [ ] Check that element-web has shipped to dockerhub & ghcr.io
|
||||||
- [ ] Deploy staging.element.io. [See docs.](https://handbook.element.io/books/element-web-team/page/deploying-appstagingelementio)
|
- [ ] Check that the staging [deployment](https://github.com/element-hq/element-web/actions/workflows/deploy.yml) has completed successfully
|
||||||
- [ ] Test staging.element.io
|
- [ ] Test staging.element.io
|
||||||
|
|
||||||
For final releases additionally do these steps:
|
For final releases additionally do these steps:
|
||||||
@@ -225,6 +227,9 @@ For final releases additionally do these steps:
|
|||||||
- [ ] Ensure Element Web package has shipped to packages.element.io
|
- [ ] Ensure Element Web package has shipped to packages.element.io
|
||||||
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
- [ ] Ensure Element Desktop packages have shipped to packages.element.io
|
||||||
|
|
||||||
|
If you need to roll back a deployment to staging.element.io,
|
||||||
|
you can run the `deploy.yml` automation choosing an older tag which you wish to deploy.
|
||||||
|
|
||||||
# Housekeeping
|
# Housekeeping
|
||||||
|
|
||||||
We have some manual housekeeping to do in order to prepare for the next release.
|
We have some manual housekeeping to do in order to prepare for the next release.
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ const config: Config = {
|
|||||||
"^!!raw-loader!.*": "jest-raw-loader",
|
"^!!raw-loader!.*": "jest-raw-loader",
|
||||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||||
|
// Requires ESM which is incompatible with our current Jest setup
|
||||||
|
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||||
collectCoverageFrom: [
|
collectCoverageFrom: [
|
||||||
|
|||||||
16
knip.ts
16
knip.ts
@@ -10,15 +10,16 @@ export default {
|
|||||||
"playwright/**",
|
"playwright/**",
|
||||||
"test/**",
|
"test/**",
|
||||||
"res/decoder-ring/**",
|
"res/decoder-ring/**",
|
||||||
|
"res/jitsi_external_api.min.js",
|
||||||
|
"docs/**",
|
||||||
|
// Used by jest
|
||||||
|
"__mocks__/maplibre-gl.js",
|
||||||
],
|
],
|
||||||
project: ["**/*.{js,ts,jsx,tsx}"],
|
project: ["**/*.{js,ts,jsx,tsx}"],
|
||||||
ignore: [
|
ignore: [
|
||||||
"docs/**",
|
|
||||||
"res/jitsi_external_api.min.js",
|
|
||||||
// Used by jest
|
|
||||||
"__mocks__/maplibre-gl.js",
|
|
||||||
// Keep for now
|
// Keep for now
|
||||||
"src/hooks/useLocalStorageState.ts",
|
"src/hooks/useLocalStorageState.ts",
|
||||||
|
"src/hooks/useTimeout.ts",
|
||||||
"src/components/views/elements/InfoTooltip.tsx",
|
"src/components/views/elements/InfoTooltip.tsx",
|
||||||
"src/components/views/elements/StyledCheckbox.tsx",
|
"src/components/views/elements/StyledCheckbox.tsx",
|
||||||
],
|
],
|
||||||
@@ -37,13 +38,10 @@ export default {
|
|||||||
// False positive
|
// False positive
|
||||||
"sw.js",
|
"sw.js",
|
||||||
// Used by webpack
|
// Used by webpack
|
||||||
"buffer",
|
|
||||||
"process",
|
"process",
|
||||||
"util",
|
"util",
|
||||||
// Used by workflows
|
// Embedded into webapp
|
||||||
"ts-prune",
|
"@element-hq/element-call-embedded",
|
||||||
// Required due to bug in bloom-filters https://github.com/Callidon/bloom-filters/issues/75
|
|
||||||
"@types/seedrandom",
|
|
||||||
],
|
],
|
||||||
ignoreBinaries: [
|
ignoreBinaries: [
|
||||||
// Used in scripts & workflows
|
// Used in scripts & workflows
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import * as fs from "fs";
|
|||||||
import * as childProcess from "child_process";
|
import * as childProcess from "child_process";
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
|
|
||||||
import { BuildConfig } from "./BuildConfig";
|
import { type BuildConfig } from "./BuildConfig";
|
||||||
|
|
||||||
// This expects to be run from ./scripts/install.ts
|
// This expects to be run from ./scripts/install.ts
|
||||||
|
|
||||||
@@ -23,10 +23,9 @@ const MODULES_TS_HEADER = `
|
|||||||
* You are not a salmon.
|
* You are not a salmon.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { RuntimeModule } from "@matrix-org/react-sdk-module-api/lib/RuntimeModule";
|
|
||||||
`;
|
`;
|
||||||
const MODULES_TS_DEFINITIONS = `
|
const MODULES_TS_DEFINITIONS = `
|
||||||
export const INSTALLED_MODULES: RuntimeModule[] = [];
|
export const INSTALLED_MODULES = [];
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function installer(config: BuildConfig): void {
|
export function installer(config: BuildConfig): void {
|
||||||
@@ -78,8 +77,8 @@ export function installer(config: BuildConfig): void {
|
|||||||
return; // hit the finally{} block before exiting
|
return; // hit the finally{} block before exiting
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we reach here, everything seems fine. Write modules.ts and log some output
|
// If we reach here, everything seems fine. Write modules.js and log some output
|
||||||
// Note: we compile modules.ts in two parts for developer friendliness if they
|
// Note: we compile modules.js in two parts for developer friendliness if they
|
||||||
// happen to look at it.
|
// happen to look at it.
|
||||||
console.log("The following modules have been installed: ", installedModules);
|
console.log("The following modules have been installed: ", installedModules);
|
||||||
let modulesTsHeader = MODULES_TS_HEADER;
|
let modulesTsHeader = MODULES_TS_HEADER;
|
||||||
@@ -193,5 +192,5 @@ function isModuleVersionCompatible(ourApiVersion: string, moduleApiVersion: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeModulesTs(content: string): void {
|
function writeModulesTs(content: string): void {
|
||||||
fs.writeFileSync("./src/modules.ts", content, "utf-8");
|
fs.writeFileSync("./src/modules.js", content, "utf-8");
|
||||||
}
|
}
|
||||||
|
|||||||
124
package.json
124
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.89",
|
"version": "1.11.103",
|
||||||
"description": "A feature-rich client for Matrix.org",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -22,8 +22,7 @@
|
|||||||
"LICENSE",
|
"LICENSE",
|
||||||
"README.md",
|
"README.md",
|
||||||
"AUTHORS.rst",
|
"AUTHORS.rst",
|
||||||
"package.json",
|
"package.json"
|
||||||
"contribute.json"
|
|
||||||
],
|
],
|
||||||
"style": "bundle.css",
|
"style": "bundle.css",
|
||||||
"matrix_i18n_extra_translation_funcs": [
|
"matrix_i18n_extra_translation_funcs": [
|
||||||
@@ -62,35 +61,40 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:playwright": "playwright test",
|
"test:playwright": "playwright test",
|
||||||
"test:playwright:open": "yarn test:playwright --ui",
|
"test:playwright:open": "yarn test:playwright --ui",
|
||||||
"test:playwright:screenshots": "yarn test:playwright:screenshots:build && yarn test:playwright:screenshots:run",
|
"test:playwright:screenshots": "playwright-screenshots --project=Chrome",
|
||||||
"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",
|
|
||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:unused-exports": "ts-node ./scripts/analyse_unused_exports.ts",
|
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"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"
|
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||||
|
"postinstall": "patch-package"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"oidc-client-ts": "3.1.0",
|
"**/pretty-format/react-is": "19.1.0",
|
||||||
|
"@playwright/test": "1.52.0",
|
||||||
|
"@types/react": "19.1.4",
|
||||||
|
"@types/react-dom": "19.1.5",
|
||||||
|
"oidc-client-ts": "3.2.1",
|
||||||
"jwt-decode": "4.0.0",
|
"jwt-decode": "4.0.0",
|
||||||
"caniuse-lite": "1.0.30001684",
|
"caniuse-lite": "1.0.30001717",
|
||||||
|
"testcontainers": "10.25.0",
|
||||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
|
||||||
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
"wrap-ansi": "npm:wrap-ansi@^7.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@element-hq/element-web-module-api": "1.0.0",
|
||||||
"@fontsource/inconsolata": "^5",
|
"@fontsource/inconsolata": "^5",
|
||||||
"@fontsource/inter": "^5",
|
"@fontsource/inter": "^5",
|
||||||
"@formatjs/intl-segmenter": "^11.5.7",
|
"@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.3",
|
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||||
"@matrix-org/spec": "^1.7.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/png-chunks-extract": "^1.0.2",
|
||||||
"@vector-im/compound-design-tokens": "^2.0.1",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-web": "^7.5.0",
|
"@vector-im/compound-design-tokens": "^4.0.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.38.0",
|
"@vector-im/compound-web": "^7.11.0",
|
||||||
|
"@vector-im/matrix-wysiwyg": "2.38.3",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||||
@@ -104,6 +108,7 @@
|
|||||||
"css-tree": "^3.0.0",
|
"css-tree": "^3.0.0",
|
||||||
"diff-dom": "^5.0.0",
|
"diff-dom": "^5.0.0",
|
||||||
"diff-match-patch": "^1.0.5",
|
"diff-match-patch": "^1.0.5",
|
||||||
|
"domutils": "^3.2.2",
|
||||||
"emojibase-regex": "15.3.2",
|
"emojibase-regex": "15.3.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@@ -112,20 +117,20 @@
|
|||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.3.1",
|
||||||
"html-entities": "^2.0.0",
|
"html-entities": "^2.0.0",
|
||||||
|
"html-react-parser": "^5.2.2",
|
||||||
"is-ip": "^3.1.0",
|
"is-ip": "^3.1.0",
|
||||||
"js-xxhash": "^4.0.0",
|
"js-xxhash": "^4.0.0",
|
||||||
"jsrsasign": "^11.0.0",
|
"jsrsasign": "^11.0.0",
|
||||||
"jszip": "^3.7.0",
|
"jszip": "^3.7.0",
|
||||||
"katex": "^0.16.0",
|
"katex": "^0.16.0",
|
||||||
"linkify-element": "4.2.0",
|
"linkify-react": "4.3.1",
|
||||||
"linkify-react": "4.2.0",
|
"linkify-string": "4.3.1",
|
||||||
"linkify-string": "4.2.0",
|
"linkifyjs": "4.3.1",
|
||||||
"linkifyjs": "4.2.0",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"maplibre-gl": "^4.0.0",
|
"maplibre-gl": "^5.0.0",
|
||||||
"matrix-encrypt-attachment": "^1.0.3",
|
"matrix-encrypt-attachment": "^1.0.3",
|
||||||
"matrix-events-sdk": "0.0.1",
|
"matrix-events-sdk": "0.0.1",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "37.8.0",
|
||||||
"matrix-widget-api": "^1.10.0",
|
"matrix-widget-api": "^1.10.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
@@ -133,20 +138,22 @@
|
|||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"posthog-js": "1.157.2",
|
"posthog-js": "1.242.1",
|
||||||
"qrcode": "1.5.4",
|
"qrcode": "1.5.4",
|
||||||
"re-resizable": "6.10.1",
|
"re-resizable": "6.11.2",
|
||||||
"react": "^18.3.1",
|
"react": "^19.0.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-blurhash": "^0.3.0",
|
"react-blurhash": "^0.3.0",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^19.0.0",
|
||||||
"react-focus-lock": "^2.5.1",
|
"react-focus-lock": "^2.5.1",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
|
"react-virtualized": "^9.22.5",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "2.13.1",
|
"sanitize-html": "2.16.0",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"temporal-polyfill": "^0.2.5",
|
"temporal-polyfill": "^0.3.0",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
"uuid": "^11.0.0",
|
"uuid": "^11.0.0",
|
||||||
"what-input": "^5.2.10"
|
"what-input": "^5.2.10"
|
||||||
@@ -154,13 +161,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@action-validator/cli": "^0.6.0",
|
"@action-validator/cli": "^0.6.0",
|
||||||
"@action-validator/core": "^0.6.0",
|
"@action-validator/core": "^0.6.0",
|
||||||
"@axe-core/playwright": "^4.8.1",
|
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/eslint-parser": "^7.12.10",
|
"@babel/eslint-parser": "^7.12.10",
|
||||||
"@babel/eslint-plugin": "^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-proposal-export-default-from": "^7.12.1",
|
||||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||||
"@babel/plugin-transform-class-properties": "^7.12.1",
|
"@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-logical-assignment-operators": "^7.20.7",
|
||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.12.1",
|
||||||
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
"@babel/plugin-transform-numeric-separator": "^7.12.7",
|
||||||
@@ -172,11 +180,14 @@
|
|||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||||
|
"@element-hq/element-call-embedded": "0.10.0",
|
||||||
|
"@element-hq/element-web-playwright-common": "^1.1.5",
|
||||||
"@peculiar/webcrypto": "^1.4.3",
|
"@peculiar/webcrypto": "^1.4.3",
|
||||||
"@playwright/test": "^1.40.1",
|
"@playwright/test": "^1.50.1",
|
||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@sentry/webpack-plugin": "^2.7.1",
|
"@rrweb/types": "^2.0.0-alpha.18",
|
||||||
"@stylistic/eslint-plugin": "^2.9.0",
|
"@sentry/webpack-plugin": "^3.0.0",
|
||||||
|
"@stylistic/eslint-plugin": "^4.0.0",
|
||||||
"@svgr/webpack": "^8.0.0",
|
"@svgr/webpack": "^8.0.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.4.8",
|
"@testing-library/jest-dom": "^6.4.8",
|
||||||
@@ -189,7 +200,6 @@
|
|||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/file-saver": "^2.0.3",
|
"@types/file-saver": "^2.0.3",
|
||||||
"@types/fs-extra": "^11.0.0",
|
|
||||||
"@types/glob-to-regexp": "^0.4.1",
|
"@types/glob-to-regexp": "^0.4.1",
|
||||||
"@types/jest": "29.5.12",
|
"@types/jest": "29.5.12",
|
||||||
"@types/jitsi-meet": "^2.0.2",
|
"@types/jitsi-meet": "^2.0.2",
|
||||||
@@ -202,24 +212,24 @@
|
|||||||
"@types/node-fetch": "^2.6.2",
|
"@types/node-fetch": "^2.6.2",
|
||||||
"@types/pako": "^2.0.0",
|
"@types/pako": "^2.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "19.1.4",
|
||||||
"@types/react-beautiful-dnd": "^13.0.0",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "19.1.5",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "2.13.0",
|
"@types/sanitize-html": "2.16.0",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/tar-js": "^0.3.5",
|
"@types/tar-js": "^0.3.5",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.19.0",
|
||||||
"babel-jest": "^29.0.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",
|
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||||
"blob-polyfill": "^9.0.0",
|
"blob-polyfill": "^9.0.0",
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"concurrently": "^9.0.0",
|
"concurrently": "^9.0.0",
|
||||||
"copy-webpack-plugin": "^12.0.0",
|
"copy-webpack-plugin": "^13.0.0",
|
||||||
"core-js": "^3.38.1",
|
"core-js": "^3.38.1",
|
||||||
"cronstrue": "^2.41.0",
|
"cronstrue": "^2.41.0",
|
||||||
"css-loader": "^7.0.0",
|
"css-loader": "^7.0.0",
|
||||||
@@ -227,21 +237,21 @@
|
|||||||
"dotenv": "^16.0.2",
|
"dotenv": "^16.0.2",
|
||||||
"eslint": "8.57.1",
|
"eslint": "8.57.1",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-deprecate": "0.8.5",
|
"eslint-plugin-deprecate": "0.8.5",
|
||||||
"eslint-plugin-import": "^2.25.4",
|
"eslint-plugin-import": "^2.25.4",
|
||||||
"eslint-plugin-jest": "^28.0.0",
|
"eslint-plugin-jest": "^28.0.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||||
"eslint-plugin-matrix-org": "^2.0.2",
|
"eslint-plugin-matrix-org": "^2.0.2",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
|
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^5.0.0",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
"fetch-mock-jest": "^1.5.1",
|
"fetch-mock-jest": "^1.5.1",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"fs-extra": "^11.0.0",
|
|
||||||
"glob": "^11.0.0",
|
"glob": "^11.0.0",
|
||||||
"html-webpack-plugin": "^5.5.3",
|
"html-webpack-plugin": "^5.5.3",
|
||||||
"husky": "^9.0.0",
|
"husky": "^9.0.0",
|
||||||
@@ -252,15 +262,15 @@
|
|||||||
"jest-raw-loader": "^1.0.1",
|
"jest-raw-loader": "^1.0.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"knip": "^5.36.2",
|
"knip": "^5.36.2",
|
||||||
"lint-staged": "^15.0.2",
|
"lint-staged": "^16.0.0",
|
||||||
"mailhog": "^4.16.0",
|
|
||||||
"matrix-web-i18n": "^3.2.1",
|
"matrix-web-i18n": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.9.0",
|
"mini-css-extract-plugin": "2.9.2",
|
||||||
"minimist": "^1.2.6",
|
"minimist": "^1.2.6",
|
||||||
"modernizr": "^3.12.0",
|
"modernizr": "^3.12.0",
|
||||||
"node-fetch": "^2.6.7",
|
"node-fetch": "^2.6.7",
|
||||||
"playwright-core": "^1.45.1",
|
"patch-package": "^8.0.0",
|
||||||
"postcss": "8.4.38",
|
"playwright-core": "^1.51.0",
|
||||||
|
"postcss": "8.4.46",
|
||||||
"postcss-easings": "^4.0.0",
|
"postcss-easings": "^4.0.0",
|
||||||
"postcss-hexrgba": "2.1.0",
|
"postcss-hexrgba": "2.1.0",
|
||||||
"postcss-import": "16.1.0",
|
"postcss-import": "16.1.0",
|
||||||
@@ -270,26 +280,27 @@
|
|||||||
"postcss-preset-env": "^10.0.0",
|
"postcss-preset-env": "^10.0.0",
|
||||||
"postcss-scss": "^4.0.4",
|
"postcss-scss": "^4.0.4",
|
||||||
"postcss-simple-vars": "^7.0.1",
|
"postcss-simple-vars": "^7.0.1",
|
||||||
"prettier": "3.4.2",
|
"prettier": "3.5.3",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"stylelint": "^16.1.0",
|
"stylelint": "^16.13.0",
|
||||||
"stylelint-config-standard": "^36.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||||
"terser-webpack-plugin": "^5.3.9",
|
"terser-webpack-plugin": "^5.3.9",
|
||||||
|
"testcontainers": "^10.20.0",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"ts-prune": "^0.10.3",
|
"typescript": "5.8.3",
|
||||||
"typescript": "5.7.2",
|
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
"webpack-cli": "^5.0.0",
|
"webpack-cli": "^6.0.0",
|
||||||
"webpack-dev-server": "^5.0.0",
|
"webpack-dev-server": "^5.0.0",
|
||||||
|
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||||
"webpack-version-file-plugin": "^0.5.0",
|
"webpack-version-file-plugin": "^0.5.0",
|
||||||
"yaml": "^2.3.3"
|
"yaml": "^2.3.3"
|
||||||
},
|
},
|
||||||
@@ -300,5 +311,6 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
},
|
||||||
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||||
}
|
}
|
||||||
|
|||||||
13
patches/@matrix-org+react-sdk-module-api+2.5.0.patch
Normal file
13
patches/@matrix-org+react-sdk-module-api+2.5.0.patch
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||||
|
index 917a7fc..a2710c6 100644
|
||||||
|
--- a/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||||
|
+++ b/node_modules/@matrix-org/react-sdk-module-api/lib/ModuleApi.d.ts
|
||||||
|
@@ -37,7 +37,7 @@ export interface ModuleApi {
|
||||||
|
* @returns Whether the user submitted the dialog or closed it, and the model returned by the
|
||||||
|
* dialog component if submitted.
|
||||||
|
*/
|
||||||
|
- openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||||
|
+ openDialog<M extends object, P extends DialogProps = DialogProps, C extends DialogContent<P> = DialogContent<P>>(initialTitleOrOptions: string | ModuleUiDialogOptions, body: (props: P, ref: React.RefObject<C | null>) => React.ReactNode, props?: Omit<P, keyof DialogProps>): Promise<{
|
||||||
|
didOkOrSubmit: boolean;
|
||||||
|
model: M;
|
||||||
|
}>;
|
||||||
31
patches/@types+react+19.1.4.patch
Normal file
31
patches/@types+react+19.1.4.patch
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
|
||||||
|
index d3318dc..c2b2c77 100644
|
||||||
|
--- a/node_modules/@types/react/index.d.ts
|
||||||
|
+++ b/node_modules/@types/react/index.d.ts
|
||||||
|
@@ -134,7 +134,7 @@ declare namespace React {
|
||||||
|
props: P,
|
||||||
|
) => ReactNode | Promise<ReactNode>)
|
||||||
|
// constructor signature must match React.Component
|
||||||
|
- | (new(props: P) => Component<any, any>);
|
||||||
|
+ | (new(props: P, context?: any) => Component<any, any>);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by {@link createRef}, or {@link useRef} when passed `null`.
|
||||||
|
@@ -945,7 +945,7 @@ declare namespace React {
|
||||||
|
context: unknown;
|
||||||
|
|
||||||
|
// Keep in sync with constructor signature of JSXElementConstructor and ComponentClass.
|
||||||
|
- constructor(props: P);
|
||||||
|
+ constructor(props: P, context?: unknown);
|
||||||
|
|
||||||
|
// We MUST keep setState() as a unified signature because it allows proper checking of the method return type.
|
||||||
|
// See: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18365#issuecomment-351013257
|
||||||
|
@@ -1117,7 +1117,7 @@ declare namespace React {
|
||||||
|
*/
|
||||||
|
interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> {
|
||||||
|
// constructor signature must match React.Component
|
||||||
|
- new(props: P): Component<P, S>;
|
||||||
|
+ new(props: P, context?: any): Component<P, S>;
|
||||||
|
/**
|
||||||
|
* Ignored by React.
|
||||||
|
* @deprecated Only kept in types for backwards compatibility. Will be removed in a future major release.
|
||||||
22
patches/react-blurhash+0.3.0.patch
Normal file
22
patches/react-blurhash+0.3.0.patch
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
diff --git a/node_modules/react-blurhash/dist/index.d.ts b/node_modules/react-blurhash/dist/index.d.ts
|
||||||
|
index 3adbd0a..32e8c13 100644
|
||||||
|
--- a/node_modules/react-blurhash/dist/index.d.ts
|
||||||
|
+++ b/node_modules/react-blurhash/dist/index.d.ts
|
||||||
|
@@ -19,7 +19,7 @@ declare class Blurhash extends React.PureComponent<Props$1> {
|
||||||
|
resolutionY: number;
|
||||||
|
};
|
||||||
|
componentDidUpdate(): void;
|
||||||
|
- render(): JSX.Element;
|
||||||
|
+ render(): React.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type Props = React.CanvasHTMLAttributes<HTMLCanvasElement> & {
|
||||||
|
@@ -37,7 +37,7 @@ declare class BlurhashCanvas extends React.PureComponent<Props> {
|
||||||
|
componentDidUpdate(): void;
|
||||||
|
handleRef: (canvas: HTMLCanvasElement) => void;
|
||||||
|
draw: () => void;
|
||||||
|
- render(): JSX.Element;
|
||||||
|
+ render(): React.JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Blurhash, BlurhashCanvas };
|
||||||
@@ -8,19 +8,25 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { defineConfig, devices } from "@playwright/test";
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
import { type WorkerOptions } from "./playwright/services";
|
||||||
|
|
||||||
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
const baseURL = process.env["BASE_URL"] ?? "http://localhost:8080";
|
||||||
|
|
||||||
export default defineConfig({
|
const chromeProject = {
|
||||||
projects: [
|
|
||||||
{
|
|
||||||
name: "Chrome",
|
|
||||||
use: {
|
|
||||||
...devices["Desktop Chrome"],
|
...devices["Desktop Chrome"],
|
||||||
channel: "chromium",
|
channel: "chromium",
|
||||||
permissions: ["clipboard-write", "clipboard-read", "microphone"],
|
permissions: ["clipboard-write", "clipboard-read", "microphone"],
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
|
args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream", "--mute-audio"],
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig<WorkerOptions>({
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "Chrome",
|
||||||
|
use: {
|
||||||
|
...chromeProject,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -48,6 +54,22 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
ignoreSnapshots: true,
|
ignoreSnapshots: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Dendrite",
|
||||||
|
use: {
|
||||||
|
...chromeProject,
|
||||||
|
homeserverType: "dendrite",
|
||||||
|
},
|
||||||
|
ignoreSnapshots: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pinecone",
|
||||||
|
use: {
|
||||||
|
...chromeProject,
|
||||||
|
homeserverType: "pinecone",
|
||||||
|
},
|
||||||
|
ignoreSnapshots: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
use: {
|
use: {
|
||||||
viewport: { width: 1280, height: 720 },
|
viewport: { width: 1280, height: 720 },
|
||||||
@@ -61,6 +83,7 @@ export default defineConfig({
|
|||||||
url: `${baseURL}/config.json`,
|
url: `${baseURL}/config.json`,
|
||||||
reuseExistingServer: true,
|
reuseExistingServer: true,
|
||||||
timeout: (process.env.CI ? 30 : 120) * 1000,
|
timeout: (process.env.CI ? 30 : 120) * 1000,
|
||||||
|
stdout: "pipe",
|
||||||
},
|
},
|
||||||
testDir: "playwright/e2e",
|
testDir: "playwright/e2e",
|
||||||
outputDir: "playwright/test-results",
|
outputDir: "playwright/test-results",
|
||||||
|
|||||||
12
playwright/@types/playwright-core.d.ts
vendored
12
playwright/@types/playwright-core.d.ts
vendored
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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"]
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
npx playwright test --update-snapshots --reporter line $@
|
|
||||||
@@ -123,7 +123,7 @@ test.describe("Landmark navigation tests", () => {
|
|||||||
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
await expect(page.getByText("Bob joined the room")).toBeVisible();
|
||||||
|
|
||||||
// Close the room
|
// Close the room
|
||||||
page.goto("/#/home");
|
await page.goto("/#/home");
|
||||||
|
|
||||||
// Pressing Control+F6 will first focus the space button
|
// Pressing Control+F6 will first focus the space button
|
||||||
await page.keyboard.press("ControlOrMeta+F6");
|
await page.keyboard.press("ControlOrMeta+F6");
|
||||||
|
|||||||
@@ -13,13 +13,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
startHomeserverOpts: "guest-enabled",
|
synapseConfig: {
|
||||||
config: async ({ homeserver }, use) => {
|
allow_guest_access: true,
|
||||||
await use({
|
|
||||||
default_server_config: {
|
|
||||||
"m.homeserver": { base_url: homeserver.config.baseUrl },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import type { Locator, Page } from "@playwright/test";
|
|||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { Layout } from "../../../src/settings/enums/Layout";
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
|
|
||||||
|
// Find and click "Reply" button
|
||||||
|
const clickButtonReply = async (tile: Locator) => {
|
||||||
|
await expect(async () => {
|
||||||
|
await tile.hover();
|
||||||
|
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||||
|
}).toPass();
|
||||||
|
};
|
||||||
|
|
||||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||||
test.use({
|
test.use({
|
||||||
@@ -222,8 +230,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
|
|
||||||
// Find and click "Reply" button on MessageActionBar
|
// Find and click "Reply" button on MessageActionBar
|
||||||
const tile = page.locator(".mx_EventTile_last");
|
const tile = page.locator(".mx_EventTile_last");
|
||||||
await tile.hover();
|
await clickButtonReply(tile);
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
|
||||||
|
|
||||||
// Reply to the player with another audio file
|
// Reply to the player with another audio file
|
||||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||||
@@ -251,19 +258,12 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
|
|
||||||
const tile = page.locator(".mx_EventTile_last");
|
const tile = page.locator(".mx_EventTile_last");
|
||||||
|
|
||||||
// Find and click "Reply" button
|
|
||||||
const clickButtonReply = async () => {
|
|
||||||
await tile.scrollIntoViewIfNeeded();
|
|
||||||
await tile.hover();
|
|
||||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
|
||||||
};
|
|
||||||
|
|
||||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||||
|
|
||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
await clickButtonReply();
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
// Reply to the player with another audio file
|
// Reply to the player with another audio file
|
||||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||||
@@ -271,7 +271,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||||||
// Assert that the audio player is rendered
|
// Assert that the audio player is rendered
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||||
|
|
||||||
await clickButtonReply();
|
await clickButtonReply(tile);
|
||||||
|
|
||||||
// Reply to the player with yet another audio file to create a reply chain
|
// Reply to the player with yet another audio file to create a reply chain
|
||||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ test.describe("HTML Export", () => {
|
|||||||
async ({ page, app, room }) => {
|
async ({ page, app, room }) => {
|
||||||
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
// Set a fixed time rather than masking off the line with the time in it: we don't need to worry
|
||||||
// about the width changing and we can actually test this line looks correct.
|
// about the width changing and we can actually test this line looks correct.
|
||||||
page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
await page.clock.setSystemTime(new Date("2024-01-01T00:00:00Z"));
|
||||||
|
|
||||||
// Send a bunch of messages to populate the room
|
// Send a bunch of messages to populate the room
|
||||||
for (let i = 1; i < 10; i++) {
|
for (let i = 1; i < 10; i++) {
|
||||||
|
|||||||
@@ -79,9 +79,8 @@ test.describe("Composer", () => {
|
|||||||
// Enter some more text, then send the message
|
// Enter some more text, then send the message
|
||||||
await page.getByRole("textbox").pressSequentially("this is the spoiler text ");
|
await page.getByRole("textbox").pressSequentially("this is the spoiler text ");
|
||||||
await page.getByRole("button", { name: "Send message" }).click();
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
// Check that a spoiler item has appeared in the timeline and locator the spoiler command text
|
// Check that a spoiler item has appeared in the timeline and contains the spoiler text
|
||||||
await expect(page.locator("button.mx_EventTile_spoiler")).toBeVisible();
|
await expect(page.locator("button.mx_EventTile_spoiler")).toHaveText("this is the spoiler text");
|
||||||
await expect(page.getByText("this is the spoiler text")).toBeVisible();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -166,7 +165,7 @@ test.describe("Composer", () => {
|
|||||||
// Type another
|
// Type another
|
||||||
await page.locator("div[contenteditable=true]").pressSequentially("my message 1");
|
await page.locator("div[contenteditable=true]").pressSequentially("my message 1");
|
||||||
// Send message
|
// Send message
|
||||||
page.locator("div[contenteditable=true]").press("Enter");
|
await page.locator("div[contenteditable=true]").press("Enter");
|
||||||
// It was sent
|
// It was sent
|
||||||
await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible();
|
await expect(page.locator(".mx_EventTile_last .mx_EventTile_body").getByText("my message 1")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ test.describe("Create Room", () => {
|
|||||||
// Submit
|
// Submit
|
||||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/#\/room\/#test-room-1:localhost/);
|
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||||
const header = page.locator(".mx_RoomHeader");
|
const header = page.locator(".mx_RoomHeader");
|
||||||
await expect(header).toContainText(name);
|
await expect(header).toContainText(name);
|
||||||
});
|
});
|
||||||
|
|||||||
99
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
99
playwright/e2e/crypto/backups-mas.spec.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
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 { test, expect } from "../../element-web-test";
|
||||||
|
import { registerAccountMas } from "../oidc";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { TestClientServerAPI } from "../csAPI";
|
||||||
|
import { masHomeserver } from "../../plugins/homeserver/synapse/masHomeserver.ts";
|
||||||
|
import { checkDeviceIsConnectedKeyBackup } from "./utils";
|
||||||
|
|
||||||
|
// These tests register an account with MAS because then we go through the "normal" registration flow
|
||||||
|
// and crypto gets set up. Using the 'user' fixture create a user and synthesizes an existing login,
|
||||||
|
// which is faster but leaves us without crypto set up.
|
||||||
|
test.use(masHomeserver);
|
||||||
|
test.describe("Encryption state after registration", () => {
|
||||||
|
test.skip(isDendrite, "does not yet support MAS");
|
||||||
|
|
||||||
|
test("Key backup is enabled by default", async ({ page, mailpitClient, app }, testInfo) => {
|
||||||
|
await page.goto("/#/login");
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||||
|
|
||||||
|
// Wait for the ui to load
|
||||||
|
await expect(page.locator(".mx_MatrixChat")).toBeVisible();
|
||||||
|
|
||||||
|
// Recovery is not set up yet
|
||||||
|
await checkDeviceIsConnectedKeyBackup(app, "1", true, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("user is prompted to set up recovery", async ({ page, mailpitClient, app }, testInfo) => {
|
||||||
|
await page.goto("/#/login");
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await registerAccountMas(page, mailpitClient, `alice_${testInfo.testId}`, "alice@email.com", "Pa$sW0rD!");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||||
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Key backup reset from elsewhere", () => {
|
||||||
|
test.skip(isDendrite, "does not yet support MAS");
|
||||||
|
|
||||||
|
test("Key backup is disabled when reset from elsewhere", async ({
|
||||||
|
page,
|
||||||
|
mailpitClient,
|
||||||
|
request,
|
||||||
|
homeserver,
|
||||||
|
}, testInfo) => {
|
||||||
|
const testUsername = `alice_${testInfo.testId}`;
|
||||||
|
const testPassword = "Pa$sW0rD!";
|
||||||
|
|
||||||
|
// there's a delay before keys are uploaded so the error doesn't appear immediately: use a fake
|
||||||
|
// clock so we can skip the delay
|
||||||
|
await page.clock.install();
|
||||||
|
|
||||||
|
await page.goto("/#/login");
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await registerAccountMas(page, mailpitClient, testUsername, "alice@email.com", testPassword);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
||||||
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
const accessToken = await page.evaluate(() => window.mxMatrixClientPeg.get().getAccessToken());
|
||||||
|
|
||||||
|
const csAPI = new TestClientServerAPI(request, homeserver, accessToken);
|
||||||
|
|
||||||
|
const backupInfo = await csAPI.getCurrentBackupInfo();
|
||||||
|
|
||||||
|
await csAPI.deleteBackupVersion(backupInfo.version);
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
|
||||||
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
|
||||||
|
await page.getByRole("button", { name: "Send message" }).click();
|
||||||
|
|
||||||
|
// Should be the message we sent plus the room creation event
|
||||||
|
await expect(page.locator(".mx_EventTile")).toHaveCount(2);
|
||||||
|
await expect(
|
||||||
|
page.locator(".mx_RoomView_MessageList > .mx_EventTile_last .mx_EventTile_receiptSent"),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Wait for it to try uploading the key
|
||||||
|
await page.clock.fastForward(20000);
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { level: 1, name: "New Recovery Method" })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,144 +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 { type Page } from "@playwright/test";
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
import { test as masTest, registerAccountMas } from "../oidc";
|
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
|
||||||
|
|
||||||
async function expectBackupVersionToBe(page: Page, version: string) {
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
|
||||||
version + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
masTest.describe("Encryption state after registration", () => {
|
|
||||||
masTest.skip(isDendrite, "does not yet support MAS");
|
|
||||||
|
|
||||||
masTest("Key backup is enabled by default", async ({ page, mailhog, app }) => {
|
|
||||||
await page.goto("/#/login");
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
|
||||||
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
expect(page.getByText("This session is backing up your keys.")).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
masTest("user is prompted to set up recovery", async ({ page, mailhog, app }) => {
|
|
||||||
await page.goto("/#/login");
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await registerAccountMas(page, mailhog.api, "alice", "alice@email.com", "Pa$sW0rD!");
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add room" }).click();
|
|
||||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Name" }).fill("test room");
|
|
||||||
await page.getByRole("button", { name: "Create room" }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Set up recovery" })).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("Backups", () => {
|
|
||||||
test.use({
|
|
||||||
displayName: "Hanako",
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
"Create, delete and recreate a keys backup",
|
|
||||||
{ tag: "@no-webkit" },
|
|
||||||
async ({ page, user, app }, workerInfo) => {
|
|
||||||
// Create a backup
|
|
||||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
|
|
||||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
|
||||||
|
|
||||||
// It's the first time and secure storage is not set up, so it will create one
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
|
||||||
// copy the recovery key to use it later
|
|
||||||
const securityKey = await app.getClipboard();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
|
||||||
|
|
||||||
// Open the settings again
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
|
|
||||||
// expand the advanced section to see the active version in the reports
|
|
||||||
await page
|
|
||||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
|
||||||
.locator("..")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expectBackupVersionToBe(page, "1");
|
|
||||||
|
|
||||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
|
||||||
// Delete it
|
|
||||||
await currentDialogLocator.getByTestId("dialog-primary-button").click(); // Click "Delete Backup"
|
|
||||||
|
|
||||||
// Create another
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByLabel("Security Key").fill(securityKey);
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
// Should be successful
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Success!" })).toBeVisible();
|
|
||||||
await currentDialogLocator.getByRole("button", { name: "OK", exact: true }).click();
|
|
||||||
|
|
||||||
// Open the settings again
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
|
|
||||||
// expand the advanced section to see the active version in the reports
|
|
||||||
await page
|
|
||||||
.locator(".mx_Dialog .mx_SettingsSubsection_content details .mx_SecureBackupPanel_advanced")
|
|
||||||
.locator("..")
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expectBackupVersionToBe(page, "2");
|
|
||||||
|
|
||||||
// ==
|
|
||||||
// Ensure that if you don't have the secret storage passphrase the backup won't be created
|
|
||||||
// ==
|
|
||||||
|
|
||||||
// First delete version 2
|
|
||||||
await securityTab.getByRole("button", { name: "Delete Backup", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Delete Backup" })).toBeVisible();
|
|
||||||
// Click "Delete Backup"
|
|
||||||
await currentDialogLocator.getByTestId("dialog-primary-button").click();
|
|
||||||
|
|
||||||
// Try to create another
|
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Security Key" })).toBeVisible();
|
|
||||||
// But cancel the security key dialog, to simulate not having the secret storage passphrase
|
|
||||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Starting backup…" })).toBeVisible();
|
|
||||||
// check that it failed
|
|
||||||
await expect(currentDialogLocator.getByText("Unable to create key backup")).toBeVisible();
|
|
||||||
// cancel
|
|
||||||
await currentDialogLocator.getByTestId("dialog-cancel-button").click();
|
|
||||||
|
|
||||||
// go back to the settings to check that no backup was created (the setup button should still be there)
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await expect(securityTab.getByRole("button", { name: "Set up", exact: true })).toBeVisible();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -8,8 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { logIntoElement } from "./utils";
|
import { logIntoElement } from "./utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Complete security", () => {
|
test.describe("Complete security", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Jeff",
|
displayName: "Jeff",
|
||||||
});
|
});
|
||||||
@@ -19,9 +21,9 @@ test.describe("Complete security", () => {
|
|||||||
homeserver,
|
homeserver,
|
||||||
credentials,
|
credentials,
|
||||||
}) => {
|
}) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible();
|
await expect(page.getByText("Welcome Jeff", { exact: true })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// see also "Verify device during login with SAS" in `verifiction.spec.ts`.
|
// see also "Verify device during login with SAS" in `verification.spec.ts`.
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import type { Page } from "@playwright/test";
|
import type { Page } from "@playwright/test";
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { autoJoin, copyAndContinue, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
import { autoJoin, createSharedRoomWithUser, enableKeyBackup, verify } from "./utils";
|
||||||
import { Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
const checkDMRoom = async (page: Page) => {
|
const checkDMRoom = async (page: Page) => {
|
||||||
const body = page.locator(".mx_RoomView_body");
|
const body = page.locator(".mx_RoomView_body");
|
||||||
@@ -20,7 +21,7 @@ const checkDMRoom = async (page: Page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDMWithBob = async (page: Page, bob: Bot) => {
|
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.getByTestId("invite-dialog-input").fill(bob.credentials.userId);
|
||||||
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
await page.locator(".mx_InviteDialog_tile_nameStack_name").getByText("Bob").click();
|
||||||
await expect(
|
await expect(
|
||||||
@@ -67,6 +68,7 @@ const bobJoin = async (page: Page, bob: Bot) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.describe("Cryptography", function () {
|
test.describe("Cryptography", function () {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
botCreateOpts: {
|
botCreateOpts: {
|
||||||
@@ -75,8 +77,6 @@ test.describe("Cryptography", function () {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const isDeviceVerified of [true, false]) {
|
|
||||||
test.describe(`setting up secure key backup should work isDeviceVerified=${isDeviceVerified}`, () => {
|
|
||||||
/**
|
/**
|
||||||
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
* Verify that the `m.cross_signing.${keyType}` key is available on the account data on the server
|
||||||
* @param keyType
|
* @param keyType
|
||||||
@@ -86,6 +86,7 @@ test.describe("Cryptography", function () {
|
|||||||
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
(cli, keyType) => cli.getAccountDataFromServer(`m.cross_signing.${keyType}`),
|
||||||
keyType,
|
keyType,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(accountData.encrypted).toBeDefined();
|
expect(accountData.encrypted).toBeDefined();
|
||||||
const keys = Object.keys(accountData.encrypted);
|
const keys = Object.keys(accountData.encrypted);
|
||||||
const key = accountData.encrypted[keys[0]];
|
const key = accountData.encrypted[keys[0]];
|
||||||
@@ -94,33 +95,15 @@ test.describe("Cryptography", function () {
|
|||||||
expect(key.mac).toBeDefined();
|
expect(key.mac).toBeDefined();
|
||||||
}
|
}
|
||||||
|
|
||||||
test("by recovery code", async ({ page, app, user: aliceCredentials }) => {
|
test("Setting up key backup by recovery key", async ({ page, app, user: aliceCredentials }) => {
|
||||||
// Verified the device
|
|
||||||
if (isDeviceVerified) {
|
|
||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
}
|
|
||||||
|
|
||||||
await page.route("**/_matrix/client/v3/keys/signatures/upload", async (route) => {
|
await enableKeyBackup(app);
|
||||||
// We delay this API otherwise the `Setting up keys` may happen too quickly and cause flakiness
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
await route.continue();
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
// Wait for the cross signing keys to be uploaded
|
||||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
// Waiting for "Change the recovery key" button ensure that all the secrets are uploaded and cached locally
|
||||||
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
const dialog = page.locator(".mx_Dialog");
|
await expect(encryptionTab.getByRole("button", { name: "Change recovery key" })).toBeVisible();
|
||||||
// Recovery key is selected by default
|
|
||||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
|
||||||
await copyAndContinue(page);
|
|
||||||
|
|
||||||
// If the device is unverified, there should be a "Setting up keys" step; however, it
|
|
||||||
// can be quite quick, and playwright can miss it, so we can't test for it.
|
|
||||||
|
|
||||||
// Either way, we end up at a success dialog:
|
|
||||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
|
||||||
await dialog.getByRole("button", { name: "Done" }).click();
|
|
||||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Verify that the SSSS keys are in the account data stored in the server
|
// Verify that the SSSS keys are in the account data stored in the server
|
||||||
await verifyKey(app, "master");
|
await verifyKey(app, "master");
|
||||||
@@ -128,43 +111,9 @@ test.describe("Cryptography", function () {
|
|||||||
await verifyKey(app, "user_signing");
|
await verifyKey(app, "user_signing");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("by passphrase", async ({ page, app, user: aliceCredentials }) => {
|
|
||||||
// Verified the device
|
|
||||||
if (isDeviceVerified) {
|
|
||||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
await page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
|
||||||
|
|
||||||
const dialog = page.locator(".mx_Dialog");
|
|
||||||
// Select passphrase option
|
|
||||||
await dialog.getByText("Enter a Security Phrase").click();
|
|
||||||
await dialog.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Fill passphrase input
|
|
||||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
|
||||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
|
||||||
// Confirm passphrase
|
|
||||||
await dialog.locator("input").fill("new passphrase for setting up a secure key backup");
|
|
||||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
|
||||||
|
|
||||||
await copyAndContinue(page);
|
|
||||||
|
|
||||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
|
||||||
await dialog.getByRole("button", { name: "Done" }).click();
|
|
||||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
|
||||||
|
|
||||||
// Verify that the SSSS keys are in the account data stored in the server
|
|
||||||
await verifyKey(app, "master");
|
|
||||||
await verifyKey(app, "self_signing");
|
|
||||||
await verifyKey(app, "user_signing");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
test("Can reset cross-signing keys", async ({ page, app, user: aliceCredentials }) => {
|
||||||
const secretStorageKey = await enableKeyBackup(app);
|
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||||
|
await enableKeyBackup(app);
|
||||||
|
|
||||||
// Fetch the current cross-signing keys
|
// Fetch the current cross-signing keys
|
||||||
async function fetchMasterKey() {
|
async function fetchMasterKey() {
|
||||||
@@ -178,18 +127,15 @@ test.describe("Cryptography", function () {
|
|||||||
return k;
|
return k;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const masterKey1 = await fetchMasterKey();
|
const masterKey1 = await fetchMasterKey();
|
||||||
|
|
||||||
// Find the "reset cross signing" button, and click it
|
// Find "the Reset cryptographic identity" button
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
await page.locator("div.mx_CrossSigningPanel_buttonRow").getByRole("button", { name: "Reset" }).click();
|
await encryptionTab.getByRole("button", { name: "Reset cryptographic identity" }).click();
|
||||||
|
|
||||||
// Confirm
|
// Confirm
|
||||||
await page.getByRole("button", { name: "Clear cross-signing keys" }).click();
|
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
// Enter the 4S key
|
|
||||||
await page.getByPlaceholder("Security Key").fill(secretStorageKey);
|
|
||||||
await page.getByRole("button", { name: "Continue" }).click();
|
|
||||||
|
|
||||||
// Enter the password
|
// Enter the password
|
||||||
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
await page.getByPlaceholder("Password").fill(aliceCredentials.password);
|
||||||
@@ -199,9 +145,6 @@ test.describe("Cryptography", function () {
|
|||||||
const masterKey2 = await fetchMasterKey();
|
const masterKey2 = await fetchMasterKey();
|
||||||
expect(masterKey1).not.toEqual(masterKey2);
|
expect(masterKey1).not.toEqual(masterKey2);
|
||||||
}).toPass();
|
}).toPass();
|
||||||
|
|
||||||
// The dialog should have gone away
|
|
||||||
await expect(page.locator(".mx_Dialog")).toHaveCount(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ test.describe("Cryptography", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe("decryption failure messages", () => {
|
test.describe("decryption failure messages", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
|
|
||||||
test("should handle device-relative historical messages", async ({
|
test("should handle device-relative historical messages", async ({
|
||||||
homeserver,
|
homeserver,
|
||||||
page,
|
page,
|
||||||
@@ -45,7 +47,7 @@ test.describe("Cryptography", function () {
|
|||||||
await logOutOfElement(page, true);
|
await logOutOfElement(page, true);
|
||||||
|
|
||||||
// Log in again, and see how the message looks.
|
// Log in again, and see how the message looks.
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
await app.viewRoomByName("Test room");
|
await app.viewRoomByName("Test room");
|
||||||
const lastTile = page.locator(".mx_EventTile").last();
|
const lastTile = page.locator(".mx_EventTile").last();
|
||||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||||
@@ -62,7 +64,7 @@ test.describe("Cryptography", function () {
|
|||||||
|
|
||||||
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
||||||
await logOutOfElement(page);
|
await logOutOfElement(page);
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Skip verification for now" }).click();
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
|
||||||
await app.viewRoomByName("Test room");
|
await app.viewRoomByName("Test room");
|
||||||
|
|||||||
@@ -6,99 +6,198 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Locator, type Page } from "@playwright/test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
import { test as base, expect, Fixtures } from "../../element-web-test";
|
|
||||||
import { viewRoomSummaryByName } from "../right-panel/utils";
|
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { createBot, logIntoElement } from "./utils.ts";
|
||||||
|
import { type Client } from "../../pages/client.ts";
|
||||||
|
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
const test = base.extend<Fixtures>({
|
|
||||||
// eslint-disable-next-line no-empty-pattern
|
|
||||||
startHomeserverOpts: async ({}, use) => {
|
|
||||||
await use("dehydration");
|
|
||||||
},
|
|
||||||
config: async ({ homeserver, context }, use) => {
|
|
||||||
const wellKnown = {
|
|
||||||
"m.homeserver": {
|
|
||||||
base_url: homeserver.config.baseUrl,
|
|
||||||
},
|
|
||||||
"org.matrix.msc3814": true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await context.route("https://localhost/.well-known/matrix/client", async (route) => {
|
|
||||||
await route.fulfill({ json: wellKnown });
|
|
||||||
});
|
|
||||||
|
|
||||||
await use({
|
|
||||||
default_server_config: wellKnown,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ROOM_NAME = "Test room";
|
|
||||||
const NAME = "Alice";
|
const NAME = "Alice";
|
||||||
|
|
||||||
function getMemberTileByName(page: Page, name: string): Locator {
|
test.use({
|
||||||
return page.locator(`.mx_EntityTile, [title="${name}"]`);
|
displayName: NAME,
|
||||||
}
|
synapseConfig: {
|
||||||
|
experimental_features: {
|
||||||
|
msc2697_enabled: false,
|
||||||
|
msc3814_enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
test.describe("Dehydration", () => {
|
test.describe("Dehydration", () => {
|
||||||
test.skip(isDendrite, "does not yet support dehydration v2");
|
test.skip(isDendrite, "does not yet support dehydration v2");
|
||||||
|
|
||||||
test.use({
|
test("Verify device and reset creates dehydrated device", async ({ page, user, credentials, app }, workerInfo) => {
|
||||||
displayName: NAME,
|
// Verify the device by resetting the identity key, and then set up recovery (which will create SSSS, and dehydrated device)
|
||||||
});
|
|
||||||
|
|
||||||
test("Create dehydrated device", async ({ page, user, app }, workerInfo) => {
|
|
||||||
// Create a backup (which will create SSSS, and dehydrated device)
|
|
||||||
|
|
||||||
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
const securityTab = await app.settings.openUserSettings("Security & Privacy");
|
||||||
|
|
||||||
await expect(securityTab.getByRole("heading", { name: "Secure Backup" })).toBeVisible();
|
|
||||||
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
await expect(securityTab.getByText("Offline device enabled")).not.toBeVisible();
|
||||||
await securityTab.getByRole("button", { name: "Set up", exact: true }).click();
|
|
||||||
|
|
||||||
const currentDialogLocator = page.locator(".mx_Dialog");
|
await app.closeDialog();
|
||||||
|
|
||||||
// It's the first time and secure storage is not set up, so it will create one
|
// Reset the identity key
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
const settings = await app.settings.openUserSettings("Encryption");
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Security Key" })).toBeVisible();
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
// Set up recovery
|
||||||
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
await page.getByRole("button", { name: "Set up recovery" }).click();
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
const recoveryKey = await page.getByTestId("recoveryKey").innerText();
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await page.getByRole("textbox").fill(recoveryKey);
|
||||||
|
await page.getByRole("button", { name: "Finish set up" }).click();
|
||||||
|
await page.getByRole("button", { name: "Close" }).click();
|
||||||
|
|
||||||
// Open the settings again
|
await expectDehydratedDeviceEnabled(app);
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
|
||||||
|
|
||||||
// The Security tab should indicate that there is a dehydrated device present
|
|
||||||
await expect(securityTab.getByText("Offline device enabled")).toBeVisible();
|
|
||||||
|
|
||||||
await app.settings.closeDialog();
|
|
||||||
|
|
||||||
// the dehydrated device gets created with the name "Dehydrated
|
// the dehydrated device gets created with the name "Dehydrated
|
||||||
// device". We want to make sure that it is not visible as a normal
|
// device". We want to make sure that it is not visible as a normal
|
||||||
// device.
|
// device.
|
||||||
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
const sessionsTab = await app.settings.openUserSettings("Sessions");
|
||||||
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
await expect(sessionsTab.getByText("Dehydrated device")).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("'Set up recovery' creates dehydrated device", async ({ app, credentials, page }) => {
|
||||||
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
|
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 app.settings.closeDialog();
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
// now check that the user info right-panel shows the dehydrated device
|
await expectDehydratedDeviceEnabled(app);
|
||||||
// as a feature rather than as a normal device
|
});
|
||||||
await app.client.createRoom({ name: ROOM_NAME });
|
|
||||||
|
|
||||||
await viewRoomSummaryByName(page, app, ROOM_NAME);
|
test("Reset identity during login and set up recovery re-creates dehydrated device", async ({
|
||||||
|
page,
|
||||||
|
homeserver,
|
||||||
|
app,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
// Set up cross-signing and recovery
|
||||||
|
const { botClient } = await createBot(page, homeserver, credentials);
|
||||||
|
// ... and dehydration
|
||||||
|
await botClient.evaluate(async (client) => await client.getCrypto().startDehydration());
|
||||||
|
|
||||||
await page.locator(".mx_RightPanel").getByRole("menuitem", { name: "People" }).click();
|
const initialDehydratedDeviceIds = await getDehydratedDeviceIds(botClient);
|
||||||
await expect(page.locator(".mx_MemberList")).toBeVisible();
|
expect(initialDehydratedDeviceIds.length).toBe(1);
|
||||||
|
|
||||||
await getMemberTileByName(page, NAME).click();
|
await botClient.evaluate(async (client) => client.stopClient());
|
||||||
await page.locator(".mx_UserInfo_devices .mx_UserInfo_expand").click();
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Offline device enabled")).toBeVisible();
|
// Log in our client
|
||||||
await expect(page.locator(".mx_UserInfo_devices").getByText("Dehydrated device")).not.toBeVisible();
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
|
// Oh no, we forgot our recovery key - reset our identity
|
||||||
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Reset all" }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Are you sure you want to reset your identity?" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// And set up recovery
|
||||||
|
const settings = await app.settings.openUserSettings("Encryption");
|
||||||
|
await settings.getByRole("button", { name: "Set up recovery" }).click();
|
||||||
|
await settings.getByRole("button", { name: "Continue" }).click();
|
||||||
|
const recoveryKey = await settings.getByTestId("recoveryKey").innerText();
|
||||||
|
await settings.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await settings.getByRole("textbox").fill(recoveryKey);
|
||||||
|
await settings.getByRole("button", { name: "Finish set up" }).click();
|
||||||
|
|
||||||
|
// There should be a brand new dehydrated device
|
||||||
|
await expectDehydratedDeviceEnabled(app);
|
||||||
|
});
|
||||||
|
|
||||||
|
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[]> {
|
||||||
|
return await client.evaluate(async (client) => {
|
||||||
|
const userId = client.getUserId();
|
||||||
|
const devices = await client.getCrypto().getUserDeviceInfo([userId]);
|
||||||
|
return Array.from(
|
||||||
|
devices
|
||||||
|
.get(userId)
|
||||||
|
.values()
|
||||||
|
.filter((d) => d.dehydrated)
|
||||||
|
.map((d) => d.deviceId),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wait for our user to have a dehydrated device */
|
||||||
|
async function expectDehydratedDeviceEnabled(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(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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,11 +15,13 @@ import {
|
|||||||
awaitVerifier,
|
awaitVerifier,
|
||||||
checkDeviceIsConnectedKeyBackup,
|
checkDeviceIsConnectedKeyBackup,
|
||||||
checkDeviceIsCrossSigned,
|
checkDeviceIsCrossSigned,
|
||||||
|
createBot,
|
||||||
doTwoWaySasVerification,
|
doTwoWaySasVerification,
|
||||||
logIntoElement,
|
logIntoElement,
|
||||||
waitForVerificationRequest,
|
waitForVerificationRequest,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { Bot } from "../../pages/bot";
|
import { type Bot } from "../../pages/bot";
|
||||||
|
import { Toasts } from "../../pages/toasts.ts";
|
||||||
|
|
||||||
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||||
let aliceBotClient: Bot;
|
let aliceBotClient: Bot;
|
||||||
@@ -28,29 +30,9 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
let expectedBackupVersion: string;
|
let expectedBackupVersion: string;
|
||||||
|
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
// Visit the login page of the app, to load the matrix sdk
|
const res = await createBot(page, homeserver, credentials, true);
|
||||||
await page.goto("/#/login");
|
aliceBotClient = res.botClient;
|
||||||
|
expectedBackupVersion = res.expectedBackupVersion;
|
||||||
// wait for the page to load
|
|
||||||
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
|
||||||
|
|
||||||
// Create a new device for alice
|
|
||||||
aliceBotClient = new Bot(page, homeserver, {
|
|
||||||
bootstrapCrossSigning: true,
|
|
||||||
bootstrapSecretStorage: true,
|
|
||||||
});
|
|
||||||
aliceBotClient.setCredentials(credentials);
|
|
||||||
|
|
||||||
// Backup is prepared in the background. Poll until it is ready.
|
|
||||||
const botClientHandle = await aliceBotClient.prepareClient();
|
|
||||||
await expect
|
|
||||||
.poll(async () => {
|
|
||||||
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
|
||||||
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
|
||||||
);
|
|
||||||
return expectedBackupVersion;
|
|
||||||
})
|
|
||||||
.not.toBe(null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
// Click the "Verify with another device" button, and have the bot client auto-accept it.
|
||||||
@@ -66,7 +48,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
// Launch the verification request between alice and the bot
|
||||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||||
@@ -87,13 +69,58 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
// as we need to wait for the secret gossiping to happen.
|
||||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
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 }) => {
|
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"
|
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
// Launch the verification request between alice and the bot
|
||||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||||
@@ -131,21 +158,19 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
await checkDeviceIsCrossSigned(app);
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||||
// as we need to wait for the secret gossiping to happen and the settings dialog doesn't refresh automatically.
|
|
||||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Select the security phrase
|
// Select the security phrase
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||||
|
|
||||||
// Fill the passphrase
|
// Fill the passphrase
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
await dialog.locator("input").fill("new passphrase");
|
await dialog.locator("textarea").fill("new passphrase");
|
||||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
|
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
||||||
|
|
||||||
@@ -154,21 +179,20 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Verify device with Security Key during login", async ({ page, app, credentials, homeserver }) => {
|
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
// Select the security phrase
|
// Select the security phrase
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key or Phrase" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key or Phrase" }).click();
|
||||||
|
|
||||||
// Fill the security key
|
// Fill the recovery key
|
||||||
const dialog = page.locator(".mx_Dialog");
|
const dialog = page.locator(".mx_Dialog");
|
||||||
await dialog.getByRole("button", { name: "use your Security Key" }).click();
|
|
||||||
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
|
const aliceRecoveryKey = await aliceBotClient.getRecoveryKey();
|
||||||
await dialog.locator("#mx_securityKey").fill(aliceRecoveryKey.encodedPrivateKey);
|
await dialog.locator("textarea").fill(aliceRecoveryKey.encodedPrivateKey);
|
||||||
await dialog.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
|
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Done" }).click();
|
||||||
|
|
||||||
@@ -177,11 +201,11 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
// The backup decryption key should be in cache also, as we got it directly from the 4S
|
||||||
await checkDeviceIsConnectedKeyBackup(page, expectedBackupVersion, true);
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
test("Handle incoming verification request with SAS", async ({ page, credentials, homeserver, toasts }) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
|
|
||||||
/* Dismiss "Verify this device" */
|
/* Dismiss "Verify this device" */
|
||||||
const authPage = page.locator(".mx_AuthPage");
|
const authPage = page.locator(".mx_AuthPage");
|
||||||
@@ -212,16 +236,17 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
/* on the bot side, wait for the verifier to exist ... */
|
/* on the bot side, wait for the verifier to exist ... */
|
||||||
const verifier = await awaitVerifier(botVerificationRequest);
|
const verifier = await awaitVerifier(botVerificationRequest);
|
||||||
// ... confirm ...
|
// ... confirm ...
|
||||||
botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
void botVerificationRequest.evaluate((verificationRequest) => verificationRequest.verifier.verify());
|
||||||
// ... and then check the emoji match
|
// ... and then check the emoji match
|
||||||
await doTwoWaySasVerification(page, verifier);
|
await doTwoWaySasVerification(page, verifier);
|
||||||
|
|
||||||
/* And we're all done! */
|
/* And we're all done! */
|
||||||
const infoDialog = page.locator(".mx_InfoDialog");
|
const infoDialog = page.locator(".mx_InfoDialog");
|
||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||||
await expect(
|
// We don't assert the full string as the device name is unset on Synapse but set to the user ID on Dendrite
|
||||||
infoDialog.getByText(`You've successfully verified (${aliceBotClient.credentials.deviceId})!`),
|
await expect(infoDialog.getByText(`You've successfully verified`)).toContainText(
|
||||||
).toBeVisible();
|
`(${aliceBotClient.credentials.deviceId})`,
|
||||||
|
);
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Locator } from "@playwright/test";
|
import { type Locator } from "@playwright/test";
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import {
|
import {
|
||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
logIntoElement,
|
logIntoElement,
|
||||||
logOutOfElement,
|
logOutOfElement,
|
||||||
verify,
|
verify,
|
||||||
|
waitForDevices,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage.ts";
|
import { type ElementAppPage } from "../../pages/ElementAppPage.ts";
|
||||||
|
|
||||||
test.describe("Cryptography", function () {
|
test.describe("Cryptography", function () {
|
||||||
test.use({
|
test.use({
|
||||||
@@ -66,6 +67,10 @@ test.describe("Cryptography", function () {
|
|||||||
// Bob has a second, not cross-signed, device
|
// Bob has a second, not cross-signed, device
|
||||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||||
|
|
||||||
|
// Dismiss the toasts nagging us, otherwise they get in the way of clicking the room list
|
||||||
|
await page.getByRole("button", { name: "Dismiss" }).click();
|
||||||
|
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||||
|
|
||||||
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
await bob.sendEvent(testRoomId, null, "m.room.encrypted", {
|
||||||
algorithm: "m.megolm.v1.aes-sha2",
|
algorithm: "m.megolm.v1.aes-sha2",
|
||||||
ciphertext: "the bird is in the hand",
|
ciphertext: "the bird is in the hand",
|
||||||
@@ -141,25 +146,8 @@ test.describe("Cryptography", function () {
|
|||||||
// bob deletes his second device
|
// bob deletes his second device
|
||||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||||
|
|
||||||
// wait for the logout to propagate. Workaround for https://github.com/vector-im/element-web/issues/26263 by repeatedly closing and reopening Bob's user info.
|
// wait for the logout to propagate.
|
||||||
async function awaitOneDevice(iterations = 1) {
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
const rightPanel = page.locator(".mx_RightPanel");
|
|
||||||
await rightPanel.getByTestId("base-card-back-button").click();
|
|
||||||
await rightPanel.getByText("Bob").click();
|
|
||||||
const sessionCountText = await rightPanel
|
|
||||||
.locator(".mx_UserInfo_devices")
|
|
||||||
.getByText(" session", { exact: false })
|
|
||||||
.textContent();
|
|
||||||
// cf https://github.com/vector-im/element-web/issues/26279: Element-R uses the wrong text here
|
|
||||||
if (sessionCountText != "1 session" && sessionCountText != "1 verified session") {
|
|
||||||
if (iterations >= 10) {
|
|
||||||
throw new Error(`Bob still has ${sessionCountText} after 10 iterations`);
|
|
||||||
}
|
|
||||||
await awaitOneDevice(iterations + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await awaitOneDevice();
|
|
||||||
|
|
||||||
// close and reopen the room, to get the shield to update.
|
// close and reopen the room, to get the shield to update.
|
||||||
await app.viewRoomByName("Bob");
|
await app.viewRoomByName("Bob");
|
||||||
@@ -207,7 +195,7 @@ test.describe("Cryptography", function () {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
});
|
});
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await logIntoElement(page, homeserver, aliceCredentials, securityKey);
|
await logIntoElement(page, aliceCredentials, securityKey);
|
||||||
|
|
||||||
/* go back to the test room and find Bob's message again */
|
/* go back to the test room and find Bob's message again */
|
||||||
await app.viewRoomById(testRoomId);
|
await app.viewRoomById(testRoomId);
|
||||||
@@ -282,11 +270,7 @@ test.describe("Cryptography", function () {
|
|||||||
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
// Workaround for https://github.com/element-hq/element-web/issues/28640:
|
||||||
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
// make sure that Alice has seen Bob's identity before she goes offline. We do this by opening
|
||||||
// his user info.
|
// his user info.
|
||||||
await app.toggleRoomInfoPanel();
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
const rightPanel = page.locator(".mx_RightPanel");
|
|
||||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
|
||||||
await rightPanel.getByRole("button", { name: bob.credentials!.userId }).click();
|
|
||||||
await expect(rightPanel.locator(".mx_UserInfo_devices")).toContainText("1 session");
|
|
||||||
|
|
||||||
// Our app is blocked from syncing while Bob sends his messages.
|
// Our app is blocked from syncing while Bob sends his messages.
|
||||||
await app.client.network.goOffline();
|
await app.client.network.goOffline();
|
||||||
@@ -341,7 +325,7 @@ test.describe("Cryptography", function () {
|
|||||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||||
await lastE2eIcon.focus();
|
await lastE2eIcon.focus();
|
||||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||||
"Sender's verified identity has changed",
|
"Sender's verified identity was reset",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
|
|||||||
|
|
||||||
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
|
/** Tests for the "invisible crypto" behaviour -- i.e., when the "exclude insecure devices" setting is enabled */
|
||||||
test.describe("Invisible cryptography", () => {
|
test.describe("Invisible cryptography", () => {
|
||||||
|
test.slow();
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
botCreateOpts: { displayName: "Bob" },
|
botCreateOpts: { displayName: "Bob" },
|
||||||
@@ -51,6 +52,6 @@ test.describe("Invisible cryptography", () => {
|
|||||||
/* should show an error for a message from a previously verified device */
|
/* should show an error for a message from a previously verified device */
|
||||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||||
const lastTile = page.locator(".mx_EventTile_last");
|
const lastTile = page.locator(".mx_EventTile_last");
|
||||||
await expect(lastTile).toContainText("Sender's verified identity has changed");
|
await expect(lastTile).toContainText("Sender's verified identity was reset");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } from "./utils";
|
import { createRoom, enableKeyBackup, logIntoElement, sendMessageInCurrentRoom } from "./utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Logout tests", () => {
|
test.describe("Logout tests", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
await logIntoElement(page, homeserver, credentials);
|
await logIntoElement(page, credentials);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Ask to set up recovery on logout if not setup", async ({ page, app }) => {
|
test("Ask to set up recovery on logout if not setup", async ({ page, app }) => {
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { expect, Fixtures, test as base } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("migration", { tag: "@no-webkit" }, function () {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
|
||||||
const test = base.extend<Fixtures>({
|
|
||||||
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
|
// Replace the `user` fixture with one which populates the indexeddb data before starting the app.
|
||||||
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
|
user: async ({ context, pageWithCredentials: page, credentials }, use) => {
|
||||||
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
|
await page.route(`/test_indexeddb_cryptostore_dump/*`, async (route, request) => {
|
||||||
@@ -25,9 +28,6 @@ const test = base.extend<Fixtures>({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("migration", { tag: "@no-webkit" }, function () {
|
|
||||||
test.use({ displayName: "Alice" });
|
|
||||||
|
|
||||||
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
|
test("Should support migration from legacy crypto", async ({ context, user, page }, workerInfo) => {
|
||||||
test.slow();
|
test.slow();
|
||||||
|
|
||||||
|
|||||||
167
playwright/e2e/crypto/toasts.spec.ts
Normal file
167
playwright/e2e/crypto/toasts.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
* 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 GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
import { createBot, deleteCachedSecrets, disableKeyBackup, logIntoElement } from "./utils";
|
||||||
|
import { type Bot } from "../../pages/bot";
|
||||||
|
|
||||||
|
test.describe("Key storage out of sync toast", () => {
|
||||||
|
let recoveryKey: GeneratedSecretStorageKey;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||||
|
const res = await createBot(page, homeserver, credentials);
|
||||||
|
recoveryKey = res.recoveryKey;
|
||||||
|
|
||||||
|
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
|
await deleteCachedSecrets(page);
|
||||||
|
|
||||||
|
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||||
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||||
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should prompt for recovery key if 'enter recovery key' pressed", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
|
// We need to wait for there to be two toasts as the wait below won't work in isolation:
|
||||||
|
// playwright only evaluates the 'first()' call initially, not subsequent times it checks, so
|
||||||
|
// it would always be checking the same toast, even if another one is now the first.
|
||||||
|
await expect(page.getByRole("alert")).toHaveCount(2);
|
||||||
|
await expect(page.getByRole("alert").first()).toMatchScreenshot("key-storage-out-of-sync-toast.png");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Enter recovery key" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("textbox", { name: "Recovery Key" }).fill(recoveryKey.encodedPrivateKey);
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
await expect(page.getByRole("button", { name: "Enter recovery key" })).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open settings to reset flow if 'forgot recovery key' pressed", async ({ page, app, credentials }) => {
|
||||||
|
await expect(page.getByRole("button", { name: "Enter recovery key" })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Forgot recovery key?" }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Forgot your recovery key? You’ll need to reset your identity." }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("'Turn on key storage' toast", () => {
|
||||||
|
let botClient: Bot | undefined;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, homeserver, credentials, toasts }) => {
|
||||||
|
// Set up all crypto stuff. Key storage defaults to on.
|
||||||
|
|
||||||
|
const res = await createBot(page, homeserver, credentials);
|
||||||
|
const recoveryKey = res.recoveryKey;
|
||||||
|
botClient = res.botClient;
|
||||||
|
|
||||||
|
await logIntoElement(page, credentials, recoveryKey.encodedPrivateKey);
|
||||||
|
|
||||||
|
// We won't be prompted for crypto setup unless we have an e2e room, so make one
|
||||||
|
await page.getByRole("button", { name: "Add room" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Name" }).fill("Test room");
|
||||||
|
await page.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
await toasts.rejectToast("Notifications");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not show toast if key storage is on", async ({ page, toasts }) => {
|
||||||
|
// Given the default situation after signing in
|
||||||
|
// Then no toast is shown (because key storage is on)
|
||||||
|
await toasts.assertNoToasts();
|
||||||
|
|
||||||
|
// When we reload
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
// Give the toasts time to appear
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Then still no toast is shown
|
||||||
|
await toasts.assertNoToasts();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not show toast if key storage is off because we turned it off", async ({ app, page, toasts }) => {
|
||||||
|
// Given the backup is disabled because we disabled it
|
||||||
|
await disableKeyBackup(app);
|
||||||
|
|
||||||
|
// Then no toast is shown
|
||||||
|
await toasts.assertNoToasts();
|
||||||
|
|
||||||
|
// When we reload
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
// Give the toasts time to appear
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Then still no toast is shown
|
||||||
|
await toasts.assertNoToasts();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should show toast if key storage is off but account data is missing", async ({ app, page, toasts }) => {
|
||||||
|
// Given the backup is disabled but we didn't set account data saying that is expected
|
||||||
|
await disableKeyBackup(app);
|
||||||
|
await botClient.setAccountData("m.org.matrix.custom.backup_disabled", { disabled: false });
|
||||||
|
|
||||||
|
// Wait for the account data setting to stick
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// When we enter the app
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
// Then the toast is displayed
|
||||||
|
let toast = await toasts.getToast("Turn on key storage");
|
||||||
|
|
||||||
|
// And when we click "Continue"
|
||||||
|
await toast.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// Then we see the Encryption settings dialog with an option to turn on key storage
|
||||||
|
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
|
// And when we close that
|
||||||
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|
||||||
|
// Then we see the toast again
|
||||||
|
toast = await toasts.getToast("Turn on key storage");
|
||||||
|
|
||||||
|
// And when we click "Dismiss"
|
||||||
|
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||||
|
|
||||||
|
// Then we see the "are you sure?" dialog
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { name: "Are you sure you want to keep key storage turned off?" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// And when we close it by clicking away
|
||||||
|
await page.getByTestId("dialog-background").click({ force: true, position: { x: 10, y: 10 } });
|
||||||
|
|
||||||
|
// Then we see the toast again
|
||||||
|
toast = await toasts.getToast("Turn on key storage");
|
||||||
|
|
||||||
|
// And when we click Dismiss and then "Go to Settings"
|
||||||
|
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||||
|
await page.getByRole("button", { name: "Go to Settings" }).click();
|
||||||
|
|
||||||
|
// Then we see Encryption settings again
|
||||||
|
await expect(page.getByRole("checkbox", { name: "Allow key storage" })).toBeVisible();
|
||||||
|
|
||||||
|
// And when we close that, see the toast, click Dismiss, and Yes, Dismiss
|
||||||
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
toast = await toasts.getToast("Turn on key storage");
|
||||||
|
await toast.getByRole("button", { name: "Dismiss" }).click();
|
||||||
|
await page.getByRole("button", { name: "Yes, dismiss" }).click();
|
||||||
|
|
||||||
|
// Then the toast is gone
|
||||||
|
await toasts.assertNoToasts();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,10 +8,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
import { type Preset, type Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { Page } from "@playwright/test";
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { doTwoWaySasVerification, awaitVerifier } from "./utils";
|
import { doTwoWaySasVerification, awaitVerifier, waitForDevices } from "./utils";
|
||||||
import { Client } from "../../pages/client";
|
import { type Client } from "../../pages/client";
|
||||||
|
|
||||||
test.describe("User verification", () => {
|
test.describe("User verification", () => {
|
||||||
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
// note that there are other tests that check user verification works in `crypto.spec.ts`.
|
||||||
@@ -33,13 +32,17 @@ test.describe("User verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can receive a verification request when there is no existing DM", async ({
|
test("can receive a verification request when there is no existing DM", async ({
|
||||||
|
app,
|
||||||
page,
|
page,
|
||||||
bot: bob,
|
bot: bob,
|
||||||
user: aliceCredentials,
|
user: aliceCredentials,
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
await waitForDeviceKeys(page);
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
@@ -74,7 +77,7 @@ test.describe("User verification", () => {
|
|||||||
/* on the bot side, wait for the verifier to exist ... */
|
/* on the bot side, wait for the verifier to exist ... */
|
||||||
const botVerifier = await awaitVerifier(bobVerificationRequest);
|
const botVerifier = await awaitVerifier(bobVerificationRequest);
|
||||||
// ... confirm ...
|
// ... confirm ...
|
||||||
botVerifier.evaluate((verifier) => verifier.verify());
|
void botVerifier.evaluate((verifier) => verifier.verify());
|
||||||
// ... and then check the emoji match
|
// ... and then check the emoji match
|
||||||
await doTwoWaySasVerification(page, botVerifier);
|
await doTwoWaySasVerification(page, botVerifier);
|
||||||
|
|
||||||
@@ -84,13 +87,17 @@ test.describe("User verification", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can abort emoji verification when emoji mismatch", async ({
|
test("can abort emoji verification when emoji mismatch", async ({
|
||||||
|
app,
|
||||||
page,
|
page,
|
||||||
bot: bob,
|
bot: bob,
|
||||||
user: aliceCredentials,
|
user: aliceCredentials,
|
||||||
toasts,
|
toasts,
|
||||||
room: { roomId: dmRoomId },
|
room: { roomId: dmRoomId },
|
||||||
}) => {
|
}) => {
|
||||||
await waitForDeviceKeys(page);
|
await waitForDevices(app, bob.credentials.userId, 1);
|
||||||
|
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
||||||
|
const avatar = page.getByRole("button", { name: "Avatar" });
|
||||||
|
await avatar.click();
|
||||||
|
|
||||||
// once Alice has joined, Bob starts the verification
|
// once Alice has joined, Bob starts the verification
|
||||||
const bobVerificationRequest = await bob.evaluateHandle(
|
const bobVerificationRequest = await bob.evaluateHandle(
|
||||||
@@ -154,15 +161,3 @@ async function createDMRoom(client: Client, userId: string): Promise<string> {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait until we get the other user's device keys.
|
|
||||||
* In newer rust-crypto versions, the verification request will be ignored if we
|
|
||||||
* don't have the sender's device keys.
|
|
||||||
*/
|
|
||||||
async function waitForDeviceKeys(page: Page): Promise<void> {
|
|
||||||
await expect(page.getByRole("button", { name: "Avatar" })).toBeVisible();
|
|
||||||
const avatar = await page.getByRole("button", { name: "Avatar" });
|
|
||||||
await avatar.click();
|
|
||||||
await expect(page.getByText("1 session")).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,22 +6,66 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, JSHandle, type Page } from "@playwright/test";
|
import { expect, type JSHandle, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
import type { ICreateRoomOpts, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import type {
|
import type {
|
||||||
CryptoEvent,
|
CryptoEvent,
|
||||||
EmojiMapping,
|
EmojiMapping,
|
||||||
|
GeneratedSecretStorageKey,
|
||||||
ShowSasCallbacks,
|
ShowSasCallbacks,
|
||||||
VerificationRequest,
|
VerificationRequest,
|
||||||
Verifier,
|
Verifier,
|
||||||
VerifierEvent,
|
VerifierEvent,
|
||||||
} from "matrix-js-sdk/src/crypto-api";
|
} from "matrix-js-sdk/src/crypto-api";
|
||||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||||
import { Client } from "../../pages/client";
|
import { type Client } from "../../pages/client";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { Bot } from "../../pages/bot";
|
import { Bot } from "../../pages/bot";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a bot client using the supplied credentials, and wait for the key backup to be ready.
|
||||||
|
* @param page - the playwright `page` fixture
|
||||||
|
* @param homeserver - the homeserver to use
|
||||||
|
* @param credentials - the credentials to use for the bot client
|
||||||
|
* @param usePassphrase - whether to use a passphrase when creating the recovery key
|
||||||
|
*/
|
||||||
|
export async function createBot(
|
||||||
|
page: Page,
|
||||||
|
homeserver: HomeserverInstance,
|
||||||
|
credentials: Credentials,
|
||||||
|
usePassphrase = false,
|
||||||
|
): Promise<{ botClient: Bot; recoveryKey: GeneratedSecretStorageKey; expectedBackupVersion: string }> {
|
||||||
|
// Visit the login page of the app, to load the matrix sdk
|
||||||
|
await page.goto("/#/login");
|
||||||
|
|
||||||
|
// wait for the page to load
|
||||||
|
await page.waitForSelector(".mx_AuthPage", { timeout: 30000 });
|
||||||
|
|
||||||
|
// Create a new bot client
|
||||||
|
const botClient = new Bot(page, homeserver, {
|
||||||
|
bootstrapCrossSigning: true,
|
||||||
|
bootstrapSecretStorage: true,
|
||||||
|
usePassphrase,
|
||||||
|
});
|
||||||
|
botClient.setCredentials(credentials);
|
||||||
|
// Backup is prepared in the background. Poll until it is ready.
|
||||||
|
const botClientHandle = await botClient.prepareClient();
|
||||||
|
let expectedBackupVersion: string;
|
||||||
|
await expect
|
||||||
|
.poll(async () => {
|
||||||
|
expectedBackupVersion = await botClientHandle.evaluate((cli) =>
|
||||||
|
cli.getCrypto()!.getActiveSessionBackupVersion(),
|
||||||
|
);
|
||||||
|
return expectedBackupVersion;
|
||||||
|
})
|
||||||
|
.not.toBe(null);
|
||||||
|
|
||||||
|
const recoveryKey = await botClient.getRecoveryKey();
|
||||||
|
|
||||||
|
return { botClient, recoveryKey, expectedBackupVersion };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* wait for the given client to receive an incoming verification request, and automatically accept it
|
* wait for the given client to receive an incoming verification request, and automatically accept it
|
||||||
*
|
*
|
||||||
@@ -59,7 +103,7 @@ export function handleSasVerification(verifier: JSHandle<Verifier>): Promise<Emo
|
|||||||
return new Promise<EmojiMapping[]>((resolve) => {
|
return new Promise<EmojiMapping[]>((resolve) => {
|
||||||
const onShowSas = (event: ShowSasCallbacks) => {
|
const onShowSas = (event: ShowSasCallbacks) => {
|
||||||
verifier.off("show_sas" as VerifierEvent, onShowSas);
|
verifier.off("show_sas" as VerifierEvent, onShowSas);
|
||||||
event.confirm();
|
void event.confirm();
|
||||||
resolve(event.sas.emoji);
|
resolve(event.sas.emoji);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,14 +142,16 @@ export async function checkDeviceIsCrossSigned(app: ElementAppPage): Promise<voi
|
|||||||
* Check that the current device is connected to the expected key backup.
|
* Check that the current device is connected to the expected key backup.
|
||||||
* Also checks that the decryption key is known and cached locally.
|
* Also checks that the decryption key is known and cached locally.
|
||||||
*
|
*
|
||||||
* @param page - the page to check
|
* @param app -` ElementAppPage` wrapper for the playwright `Page`.
|
||||||
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
* @param expectedBackupVersion - the version of the backup we expect to be connected to.
|
||||||
* @param checkBackupKeyInCache - whether to check that the backup key is cached locally.
|
* @param checkBackupPrivateKeyInCache - whether to check that the backup decryption key is cached locally
|
||||||
|
* @param checkBackupKeyIn4S - whether to check that the backup key is stored in 4S
|
||||||
*/
|
*/
|
||||||
export async function checkDeviceIsConnectedKeyBackup(
|
export async function checkDeviceIsConnectedKeyBackup(
|
||||||
page: Page,
|
app: ElementAppPage,
|
||||||
expectedBackupVersion: string,
|
expectedBackupVersion: string,
|
||||||
checkBackupKeyInCache: boolean,
|
checkBackupPrivateKeyInCache: boolean,
|
||||||
|
checkBackupKeyIn4S: boolean = true,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
// Sanity check the given backup version: if it's null, something went wrong earlier in the test.
|
||||||
if (!expectedBackupVersion) {
|
if (!expectedBackupVersion) {
|
||||||
@@ -114,23 +160,48 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole("button", { name: "User menu" }).click();
|
const backupData = await app.client.evaluate(async (client: MatrixClient) => {
|
||||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Security & Privacy" }).click();
|
const crypto = client.getCrypto();
|
||||||
await expect(page.locator(".mx_Dialog").getByRole("button", { name: "Restore from Backup" })).toBeVisible();
|
if (!crypto) return;
|
||||||
|
|
||||||
// expand the advanced section to see the active version in the reports
|
const backupInfo = await crypto.getKeyBackupInfo();
|
||||||
await page.locator(".mx_SecureBackupPanel_advanced").locator("..").click();
|
const backupKeyIn4S = Boolean(await client.isKeyBackupKeyStored());
|
||||||
|
const backupPrivateKeyFromCache = await crypto.getSessionBackupPrivateKey();
|
||||||
|
const hasBackupPrivateKeyFromCache = Boolean(backupPrivateKeyFromCache);
|
||||||
|
const backupPrivateKeyWellFormed = backupPrivateKeyFromCache instanceof Uint8Array;
|
||||||
|
const activeBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||||
|
|
||||||
if (checkBackupKeyInCache) {
|
return {
|
||||||
const cacheDecryptionKeyStatusElement = page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(2) td");
|
backupInfo,
|
||||||
await expect(cacheDecryptionKeyStatusElement).toHaveText("cached locally, well formed");
|
hasBackupPrivateKeyFromCache,
|
||||||
|
backupPrivateKeyWellFormed,
|
||||||
|
backupKeyIn4S,
|
||||||
|
activeBackupVersion,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backupData) {
|
||||||
|
throw new Error("Crypto module is not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(5) td")).toHaveText(
|
const { backupInfo, backupKeyIn4S, hasBackupPrivateKeyFromCache, backupPrivateKeyWellFormed, activeBackupVersion } =
|
||||||
expectedBackupVersion + " (Algorithm: m.megolm_backup.v1.curve25519-aes-sha2)",
|
backupData;
|
||||||
);
|
|
||||||
|
|
||||||
await expect(page.locator(".mx_SecureBackupPanel_statusList tr:nth-child(6) td")).toHaveText(expectedBackupVersion);
|
// We have a key backup
|
||||||
|
expect(backupInfo).toBeDefined();
|
||||||
|
// The key backup version is as expected
|
||||||
|
expect(backupInfo.version).toBe(expectedBackupVersion);
|
||||||
|
// The active backup version is as expected
|
||||||
|
expect(activeBackupVersion).toBe(expectedBackupVersion);
|
||||||
|
// The backup key is stored in 4S
|
||||||
|
if (checkBackupKeyIn4S) expect(backupKeyIn4S).toBe(true);
|
||||||
|
|
||||||
|
if (checkBackupPrivateKeyInCache) {
|
||||||
|
// The backup key is available locally
|
||||||
|
expect(hasBackupPrivateKeyFromCache).toBe(true);
|
||||||
|
// The backup key is well-formed
|
||||||
|
expect(backupPrivateKeyWellFormed).toBe(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,32 +209,27 @@ export async function checkDeviceIsConnectedKeyBackup(
|
|||||||
*
|
*
|
||||||
* If a `securityKey` is given, verifies the new device using the key.
|
* If a `securityKey` is given, verifies the new device using the key.
|
||||||
*/
|
*/
|
||||||
export async function logIntoElement(
|
export async function logIntoElement(page: Page, credentials: Credentials, securityKey?: string) {
|
||||||
page: Page,
|
|
||||||
homeserver: HomeserverInstance,
|
|
||||||
credentials: Credentials,
|
|
||||||
securityKey?: string,
|
|
||||||
) {
|
|
||||||
await page.goto("/#/login");
|
await page.goto("/#/login");
|
||||||
|
|
||||||
// select homeserver
|
|
||||||
await page.getByRole("button", { name: "Edit" }).click();
|
|
||||||
await page.getByRole("textbox", { name: "Other homeserver" }).fill(homeserver.config.baseUrl);
|
|
||||||
await page.getByRole("button", { name: "Continue", exact: true }).click();
|
|
||||||
|
|
||||||
// wait for the dialog to go away
|
|
||||||
await expect(page.locator(".mx_ServerPickerDialog")).not.toBeVisible();
|
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
await page.getByRole("textbox", { name: "Username" }).fill(credentials.userId);
|
||||||
await page.getByPlaceholder("Password").fill(credentials.password);
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
await page.getByRole("button", { name: "Sign in" }).click();
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
|
||||||
// if a securityKey was given, verify the new device
|
// if a securityKey was given, verify the new device
|
||||||
if (securityKey !== undefined) {
|
if (securityKey !== undefined) {
|
||||||
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Security Key" }).click();
|
await page.locator(".mx_AuthPage").getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
// Fill in the security key
|
|
||||||
await page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
const useSecurityKey = page.locator(".mx_Dialog").getByRole("button", { name: "use your Recovery Key" });
|
||||||
await page.locator(".mx_Dialog_primary:not([disabled])", { hasText: "Continue" }).click();
|
// If the user has set a recovery *passphrase*, they'll be prompted for that first and have to click
|
||||||
|
// through to enter the recovery key which is what we have here. If they haven't, they'll be prompted
|
||||||
|
// for a recovery key straight away. We click the button if it's there so this works in both cases.
|
||||||
|
if (await useSecurityKey.isVisible()) {
|
||||||
|
await useSecurityKey.click();
|
||||||
|
}
|
||||||
|
// Fill in the recovery key
|
||||||
|
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||||
|
await page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
await page.getByRole("button", { name: "Done" }).click();
|
await page.getByRole("button", { name: "Done" }).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,18 +254,19 @@ export async function logOutOfElement(page: Page, discardKeys: boolean = false)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the security settings, and verify the current session using the security key.
|
* Open the encryption settings, and verify the current session using the recovery key.
|
||||||
*
|
*
|
||||||
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
* @param app - `ElementAppPage` wrapper for the playwright `Page`.
|
||||||
* @param securityKey - The security key (i.e., 4S key), set up during a previous session.
|
* @param securityKey - The recovery key (i.e., 4S key), set up during a previous session.
|
||||||
*/
|
*/
|
||||||
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||||
const settings = await app.settings.openUserSettings("Security & Privacy");
|
const settings = await app.settings.openUserSettings("Encryption");
|
||||||
await settings.getByRole("button", { name: "Verify this session" }).click();
|
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||||
await app.page.getByRole("button", { name: "Verify with Security Key" }).click();
|
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||||
await app.page.locator(".mx_Dialog").locator('input[type="password"]').fill(securityKey);
|
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||||
await app.page.getByRole("button", { name: "Done" }).click();
|
await app.page.getByRole("button", { name: "Done" }).click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,32 +292,95 @@ export async function doTwoWaySasVerification(page: Page, verifier: JSHandle<Ver
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the security settings and enable secure key backup.
|
* Open the encryption settings and enable key storage and recovery
|
||||||
|
* Assumes that the current device has been verified
|
||||||
*
|
*
|
||||||
* Assumes that the current device has been cross-signed (which means that we skip a step where we set it up).
|
* Returns the recovery key
|
||||||
*
|
|
||||||
* Returns the security key
|
|
||||||
*/
|
*/
|
||||||
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
export async function enableKeyBackup(app: ElementAppPage): Promise<string> {
|
||||||
await app.settings.openUserSettings("Security & Privacy");
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
await app.page.getByRole("button", { name: "Set up Secure Backup" }).click();
|
|
||||||
const dialog = app.page.locator(".mx_Dialog");
|
|
||||||
// Recovery key is selected by default
|
|
||||||
await dialog.getByRole("button", { name: "Continue" }).click({ timeout: 60000 });
|
|
||||||
|
|
||||||
// copy the text ourselves
|
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||||
const securityKey = await dialog.locator(".mx_CreateSecretStorageDialog_recoveryKey code").textContent();
|
if (!(await keyStorageToggle.isChecked())) {
|
||||||
await copyAndContinue(app.page);
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
await expect(dialog.getByText("Secure Backup successful")).toBeVisible();
|
await encryptionTab.getByRole("button", { name: "Set up recovery" }).click();
|
||||||
await dialog.getByRole("button", { name: "Done" }).click();
|
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||||
await expect(dialog.getByText("Secure Backup successful")).not.toBeVisible();
|
|
||||||
|
|
||||||
return securityKey;
|
const recoveryKey = await encryptionTab.getByTestId("recoveryKey").innerText();
|
||||||
|
await encryptionTab.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await encryptionTab.getByRole("textbox").fill(recoveryKey);
|
||||||
|
await encryptionTab.getByRole("button", { name: "Finish set up" }).click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
return recoveryKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Click on copy and continue buttons to dismiss the security key dialog
|
* Open the encryption settings and disable key storage (and recovery)
|
||||||
|
* Assumes that the current device has been verified
|
||||||
|
*/
|
||||||
|
export async function disableKeyBackup(app: ElementAppPage): Promise<void> {
|
||||||
|
const encryptionTab = await app.settings.openUserSettings("Encryption");
|
||||||
|
|
||||||
|
const keyStorageToggle = encryptionTab.getByRole("checkbox", { name: "Allow key storage" });
|
||||||
|
if (await keyStorageToggle.isChecked()) {
|
||||||
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).click();
|
||||||
|
await encryptionTab.getByRole("button", { name: "Delete key storage" }).click();
|
||||||
|
await encryptionTab.getByRole("checkbox", { name: "Allow key storage" }).isVisible();
|
||||||
|
|
||||||
|
// Wait for the update to account data to stick
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
}
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through the "Set up Secure Backup" dialog (aka the `CreateSecretStorageDialog`).
|
||||||
|
*
|
||||||
|
* Assumes the dialog is already open for some reason (see also {@link enableKeyBackup}).
|
||||||
|
*
|
||||||
|
* @param page - The playwright `Page` fixture.
|
||||||
|
* @param opts - Options object
|
||||||
|
* @param opts.accountPassword - The user's account password. If we are also resetting cross-signing, then we will need
|
||||||
|
* to upload the public cross-signing keys, which will cause the app to prompt for the password.
|
||||||
|
*
|
||||||
|
* @returns the new recovery key.
|
||||||
|
*/
|
||||||
|
export async function completeCreateSecretStorageDialog(
|
||||||
|
page: Page,
|
||||||
|
opts?: { accountPassword?: string },
|
||||||
|
): Promise<string> {
|
||||||
|
const currentDialogLocator = page.locator(".mx_Dialog");
|
||||||
|
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Set up Secure Backup" })).toBeVisible();
|
||||||
|
// "Generate a Recovery Key" is selected by default
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Save your Recovery Key" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Copy", exact: true }).click();
|
||||||
|
// copy the recovery key to use it later
|
||||||
|
const recoveryKey = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue", exact: true }).click();
|
||||||
|
|
||||||
|
// If the device is unverified, there should be a "Setting up keys" step.
|
||||||
|
// If this is not the first time we are setting up cross-signing, the app will prompt for our password; otherwise
|
||||||
|
// the step is quite quick, and playwright can miss it, so we can't test for it.
|
||||||
|
if (opts && Object.hasOwn(opts, "accountPassword")) {
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Setting up keys" })).toBeVisible();
|
||||||
|
await page.getByPlaceholder("Password").fill(opts!.accountPassword);
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Continue" }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either way, we end up at a success dialog:
|
||||||
|
await expect(currentDialogLocator.getByRole("heading", { name: "Secure Backup successful" })).toBeVisible();
|
||||||
|
await currentDialogLocator.getByRole("button", { name: "Done", exact: true }).click();
|
||||||
|
await expect(currentDialogLocator.getByText("Secure Backup successful")).not.toBeVisible();
|
||||||
|
|
||||||
|
return recoveryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click on copy and continue buttons to dismiss the recovery key dialog
|
||||||
*/
|
*/
|
||||||
export async function copyAndContinue(page: Page) {
|
export async function copyAndContinue(page: Page) {
|
||||||
await page.getByRole("button", { name: "Copy" }).click();
|
await page.getByRole("button", { name: "Copy" }).click();
|
||||||
@@ -326,7 +456,7 @@ export async function autoJoin(client: Client) {
|
|||||||
await client.evaluate((cli) => {
|
await client.evaluate((cli) => {
|
||||||
cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
cli.on(window.matrixcs.RoomMemberEvent.Membership, (event, member) => {
|
||||||
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
if (member.membership === "invite" && member.userId === cli.getUserId()) {
|
||||||
cli.joinRoom(member.roomId);
|
void cli.joinRoom(member.roomId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -385,3 +515,53 @@ export async function createSecondBotDevice(page: Page, homeserver: HomeserverIn
|
|||||||
await bobSecondDevice.prepareClient();
|
await bobSecondDevice.prepareClient();
|
||||||
return bobSecondDevice;
|
return bobSecondDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the cached secrets from the indexedDB
|
||||||
|
* This is a workaround to simulate the case where the secrets are not cached.
|
||||||
|
*/
|
||||||
|
export async function deleteCachedSecrets(page: Page) {
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
const removeCachedSecrets = new Promise((resolve) => {
|
||||||
|
const request = window.indexedDB.open("matrix-js-sdk::matrix-sdk-crypto");
|
||||||
|
request.onsuccess = (event: Event & { target: { result: IDBDatabase } }) => {
|
||||||
|
const db = event.target.result;
|
||||||
|
const request = db.transaction("core", "readwrite").objectStore("core").delete("private_identity");
|
||||||
|
request.onsuccess = () => {
|
||||||
|
db.close();
|
||||||
|
resolve(undefined);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await removeCachedSecrets;
|
||||||
|
});
|
||||||
|
await page.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until the given user has a given number of devices.
|
||||||
|
* This function will check the device keys ten times and if
|
||||||
|
* the expected number of devices were not found by then, an
|
||||||
|
* error is thrown.
|
||||||
|
*/
|
||||||
|
export async function waitForDevices(
|
||||||
|
app: ElementAppPage,
|
||||||
|
userId: string,
|
||||||
|
expectedNumberOfDevices: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await app.client.evaluate(
|
||||||
|
async (cli, { userId, expectedNumberOfDevices }) => {
|
||||||
|
for (let i = 0; i < 10; ++i) {
|
||||||
|
const userDeviceMap = await cli.getCrypto()?.getUserDeviceInfo([userId], true);
|
||||||
|
const deviceMap = userDeviceMap?.get(userId);
|
||||||
|
if (deviceMap.size === expectedNumberOfDevices) return true;
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
{ userId, expectedNumberOfDevices },
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`User ${userId} did not have ${expectedNumberOfDevices} devices within ten iterations!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
39
playwright/e2e/csAPI.ts
Normal file
39
playwright/e2e/csAPI.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A small subset of the Client-Server API used to manipulate the state of the
|
||||||
|
* account on the homeserver independently of the client under test.
|
||||||
|
*/
|
||||||
|
export class TestClientServerAPI extends ClientServerApi {
|
||||||
|
public constructor(
|
||||||
|
request: APIRequestContext,
|
||||||
|
homeserver: HomeserverInstance,
|
||||||
|
private accessToken: string,
|
||||||
|
) {
|
||||||
|
super(homeserver.baseUrl);
|
||||||
|
this.setRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getCurrentBackupInfo(): Promise<KeyBackupInfo | null> {
|
||||||
|
return this.request("GET", `/v3/room_keys/version`, this.accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the API directly to delete the given backup version
|
||||||
|
* @param version The version to delete
|
||||||
|
*/
|
||||||
|
public async deleteBackupVersion(version: string): Promise<void> {
|
||||||
|
await this.request("DELETE", `/v3/room_keys/version/${version}`, this.accessToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,13 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Locator, Page } from "@playwright/test";
|
import { type Locator, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
import type { EventType, IContent, ISendEventResponse, MsgType, Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { ElementAppPage } from "../../pages/ElementAppPage";
|
import { type ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
async function sendEvent(app: ElementAppPage, roomId: string): Promise<ISendEventResponse> {
|
async function sendEvent(app: ElementAppPage, roomId: string): Promise<ISendEventResponse> {
|
||||||
return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
|
return app.client.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||||
@@ -31,6 +32,8 @@ function mkPadding(n: number): IContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test.describe("Editing", () => {
|
test.describe("Editing", () => {
|
||||||
|
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||||
|
|
||||||
// Edit "Message"
|
// Edit "Message"
|
||||||
const editLastMessage = async (page: Page, edit: string) => {
|
const editLastMessage = async (page: Page, edit: string) => {
|
||||||
const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
const eventTile = page.locator(".mx_RoomView_MessageList .mx_EventTile_last");
|
||||||
@@ -264,7 +267,6 @@ test.describe("Editing", () => {
|
|||||||
app,
|
app,
|
||||||
room,
|
room,
|
||||||
axe,
|
axe,
|
||||||
checkA11y,
|
|
||||||
}) => {
|
}) => {
|
||||||
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
axe.disableRules("color-contrast"); // XXX: We have some known contrast issues here
|
||||||
|
|
||||||
@@ -279,7 +281,7 @@ test.describe("Editing", () => {
|
|||||||
const line = tile.locator(".mx_EventTile_line");
|
const line = tile.locator(".mx_EventTile_line");
|
||||||
await line.hover();
|
await line.hover();
|
||||||
await line.getByRole("button", { name: "Edit" }).click();
|
await line.getByRole("button", { name: "Edit" }).click();
|
||||||
await checkA11y();
|
await expect(axe).toHaveNoViolations();
|
||||||
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
const editComposer = page.getByRole("textbox", { name: "Edit message" });
|
||||||
await editComposer.pressSequentially("Foo");
|
await editComposer.pressSequentially("Foo");
|
||||||
await editComposer.press("Backspace");
|
await editComposer.press("Backspace");
|
||||||
@@ -287,7 +289,7 @@ test.describe("Editing", () => {
|
|||||||
await editComposer.press("Backspace");
|
await editComposer.press("Backspace");
|
||||||
await editComposer.press("Enter");
|
await editComposer.press("Enter");
|
||||||
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
await app.getComposerField().hover(); // XXX: move the hover to get rid of the "Edit" tooltip
|
||||||
await checkA11y();
|
await expect(axe).toHaveNoViolations();
|
||||||
}
|
}
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
page.locator(".mx_RoomView_body .mx_EventTile[data-scroll-tokens]", { hasText: "Message" }),
|
||||||
@@ -302,7 +304,6 @@ test.describe("Editing", () => {
|
|||||||
user,
|
user,
|
||||||
app,
|
app,
|
||||||
axe,
|
axe,
|
||||||
checkA11y,
|
|
||||||
bot: bob,
|
bot: bob,
|
||||||
}) => {
|
}) => {
|
||||||
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
||||||
|
|||||||
@@ -6,48 +6,65 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { type CredentialsWithDisplayName, expect, test as base } from "../../element-web-test";
|
||||||
import { selectHomeserver } from "../utils";
|
import { selectHomeserver } from "../utils";
|
||||||
|
import { emailHomeserver } from "../../plugins/homeserver/synapse/emailHomeserver.ts";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
const username = "user1234";
|
|
||||||
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
|
||||||
const password = "oETo7MPf0o";
|
|
||||||
const email = "user@nowhere.dummy";
|
const email = "user@nowhere.dummy";
|
||||||
|
|
||||||
test.describe("Forgot Password", () => {
|
const test = base.extend({
|
||||||
test.use({
|
// eslint-disable-next-line no-empty-pattern
|
||||||
startHomeserverOpts: ({ mailhog }, use) =>
|
credentials: async ({}, use, testInfo) => {
|
||||||
use({
|
await use({
|
||||||
template: "email",
|
username: `user_${testInfo.testId}`,
|
||||||
variables: {
|
// this has to be password-like enough to please zxcvbn. Needless to say it's just from pwgen.
|
||||||
SMTP_HOST: "host.containers.internal",
|
password: "oETo7MPf0o",
|
||||||
SMTP_PORT: mailhog.instance.smtpPort,
|
} as CredentialsWithDisplayName);
|
||||||
},
|
},
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.use(emailHomeserver);
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||||
|
// We point that to a guaranteed-invalid domain.
|
||||||
|
default_server_config: {
|
||||||
|
"m.homeserver": {
|
||||||
|
base_url: "https://server.invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Forgot Password", () => {
|
||||||
|
test.skip(isDendrite, "not yet wired up");
|
||||||
|
|
||||||
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
test("renders properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Sign in" }).click();
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
|
||||||
// need to select a homeserver at this stage, before entering the forgot password flow
|
// need to select a homeserver at this stage, before entering the forgot password flow
|
||||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
await selectHomeserver(page, homeserver.baseUrl);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||||
|
|
||||||
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
await expect(page.getByRole("main")).toMatchScreenshot("forgot-password.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders email verification dialog properly", { tag: "@screenshot" }, async ({ page, homeserver }) => {
|
test(
|
||||||
const user = await homeserver.registerUser(username, password);
|
"renders email verification dialog properly",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, homeserver, credentials }) => {
|
||||||
|
const user = await homeserver.registerUser(credentials.username, credentials.password);
|
||||||
|
|
||||||
await homeserver.setThreepid(user.userId, "email", email);
|
await homeserver.setThreepid(user.userId, "email", email);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Sign in" }).click();
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
await selectHomeserver(page, homeserver.baseUrl);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Forgot password?" }).click();
|
await page.getByRole("button", { name: "Forgot password?" }).click();
|
||||||
|
|
||||||
@@ -57,13 +74,14 @@ test.describe("Forgot Password", () => {
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Next" }).click();
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(password);
|
await page.getByRole("textbox", { name: "New Password", exact: true }).fill(credentials.password);
|
||||||
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(password);
|
await page.getByRole("textbox", { name: "Confirm new password", exact: true }).fill(credentials.password);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Reset password" }).click();
|
await page.getByRole("button", { name: "Reset password" }).click();
|
||||||
|
|
||||||
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
await expect(page.getByRole("button", { name: "Resend" })).toBeInViewport();
|
||||||
|
|
||||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("forgot-password-verify-email.png");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,29 +69,13 @@ async function sendActionFromIntegrationManager(
|
|||||||
await iframe.getByRole("button", { name: "Press to send action" }).click();
|
await iframe.getByRole("button", { name: "Press to send action" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clickUntilGone(page: Page, selector: string, attempt = 0) {
|
|
||||||
if (attempt === 11) {
|
|
||||||
throw new Error("clickUntilGone attempt count exceeded");
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.locator(selector).last().click();
|
|
||||||
|
|
||||||
const count = await page.locator(selector).count();
|
|
||||||
if (count > 0) {
|
|
||||||
return clickUntilGone(page, selector, ++attempt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectKickedMessage(page: Page, shouldExist: boolean) {
|
async function expectKickedMessage(page: Page, shouldExist: boolean) {
|
||||||
// Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
|
await expect(async () => {
|
||||||
// This is quite horrible but seems the most stable way of clicking 0-N buttons,
|
await page.locator(".mx_GenericEventListSummary_toggle[aria-expanded=false]").last().click();
|
||||||
// one at a time with a full re-evaluation after each click
|
|
||||||
await clickUntilGone(page, ".mx_GenericEventListSummary_toggle[aria-expanded=false]");
|
|
||||||
|
|
||||||
// Check for the event message (or lack thereof)
|
|
||||||
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
await expect(page.getByText(`${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`)).toBeVisible({
|
||||||
visible: shouldExist,
|
visible: shouldExist,
|
||||||
});
|
});
|
||||||
|
}).toPass();
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe("Integration Manager: Kick", () => {
|
test.describe("Integration Manager: Kick", () => {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ test.describe("Invite dialog", function () {
|
|||||||
"should support inviting a user to Direct Messages",
|
"should support inviting a user to Direct Messages",
|
||||||
{ tag: "@screenshot" },
|
{ tag: "@screenshot" },
|
||||||
async ({ page, app, user, bot }) => {
|
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");
|
const other = page.locator(".mx_InviteDialog_other");
|
||||||
// Assert that the header is rendered
|
// Assert that the header is rendered
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { waitForRoom } from "../utils";
|
import { waitForRoom } from "../utils";
|
||||||
import { Filter } from "../../pages/Spotlight";
|
import { Filter } from "../../pages/Spotlight";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Create Knock Room", () => {
|
test.describe("Create Knock Room", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
labsFlags: ["feature_ask_to_join"],
|
labsFlags: ["feature_ask_to_join"],
|
||||||
@@ -79,6 +81,7 @@ test.describe("Create Knock Room", () => {
|
|||||||
|
|
||||||
const spotlightDialog = await app.openSpotlight();
|
const spotlightDialog = await app.openSpotlight();
|
||||||
await spotlightDialog.filter(Filter.PublicRooms);
|
await spotlightDialog.filter(Filter.PublicRooms);
|
||||||
|
await spotlightDialog.search("Cyber");
|
||||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ import { type Visibility } from "matrix-js-sdk/src/matrix";
|
|||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { waitForRoom } from "../utils";
|
import { waitForRoom } from "../utils";
|
||||||
import { Filter } from "../../pages/Spotlight";
|
import { Filter } from "../../pages/Spotlight";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Knock Into Room", () => {
|
test.describe("Knock Into Room", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
labsFlags: ["feature_ask_to_join"],
|
labsFlags: ["feature_ask_to_join"],
|
||||||
@@ -282,6 +284,7 @@ test.describe("Knock Into Room", () => {
|
|||||||
|
|
||||||
const spotlightDialog = await app.openSpotlight();
|
const spotlightDialog = await app.openSpotlight();
|
||||||
await spotlightDialog.filter(Filter.PublicRooms);
|
await spotlightDialog.filter(Filter.PublicRooms);
|
||||||
|
await spotlightDialog.search("Cyber");
|
||||||
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
await expect(spotlightDialog.results.nth(0)).toContainText("Cybersecurity");
|
||||||
await spotlightDialog.results.nth(0).click();
|
await spotlightDialog.results.nth(0).click();
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
import { waitForRoom } from "../utils";
|
import { waitForRoom } from "../utils";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Manage Knocks", () => {
|
test.describe("Manage Knocks", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite does not have support for knocking");
|
||||||
test.use({
|
test.use({
|
||||||
displayName: "Alice",
|
displayName: "Alice",
|
||||||
labsFlags: ["feature_ask_to_join"],
|
labsFlags: ["feature_ask_to_join"],
|
||||||
@@ -50,7 +52,7 @@ test.describe("Manage Knocks", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should deny knock using bar", async ({ page, app, bot, room }) => {
|
test("should deny knock using bar", async ({ page, app, bot, room }) => {
|
||||||
bot.knockRoom(room.roomId);
|
await bot.knockRoom(room.roomId);
|
||||||
|
|
||||||
const roomKnocksBar = page.locator(".mx_RoomKnocksBar");
|
const roomKnocksBar = page.locator(".mx_RoomKnocksBar");
|
||||||
await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible();
|
await expect(roomKnocksBar.getByRole("heading", { name: "Asking to join" })).toBeVisible();
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import { Bot } from "../../pages/bot";
|
|||||||
import type { Locator, Page } from "@playwright/test";
|
import type { Locator, Page } from "@playwright/test";
|
||||||
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
import type { ElementAppPage } from "../../pages/ElementAppPage";
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
import { type Credentials } from "../../plugins/homeserver";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
|
||||||
test.describe("Lazy Loading", () => {
|
test.describe("Lazy Loading", () => {
|
||||||
|
test.skip(isDendrite, "due to a Dendrite bug https://github.com/element-hq/dendrite/issues/3488");
|
||||||
|
|
||||||
const charlies: Bot[] = [];
|
const charlies: Bot[] = [];
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
@@ -35,12 +39,18 @@ test.describe("Lazy Loading", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const name = "Lazy Loading Test";
|
const name = "Lazy Loading Test";
|
||||||
const alias = "#lltest:localhost";
|
|
||||||
const charlyMsg1 = "hi bob!";
|
const charlyMsg1 = "hi bob!";
|
||||||
const charlyMsg2 = "how's it going??";
|
const charlyMsg2 = "how's it going??";
|
||||||
let roomId: string;
|
let roomId: string;
|
||||||
|
|
||||||
async function setupRoomWithBobAliceAndCharlies(page: Page, app: ElementAppPage, bob: Bot, charlies: Bot[]) {
|
async function setupRoomWithBobAliceAndCharlies(
|
||||||
|
page: Page,
|
||||||
|
app: ElementAppPage,
|
||||||
|
user: Credentials,
|
||||||
|
bob: Bot,
|
||||||
|
charlies: Bot[],
|
||||||
|
) {
|
||||||
|
const alias = `#lltest:${user.homeServer}`;
|
||||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||||
roomId = await bob.createRoom({
|
roomId = await bob.createRoom({
|
||||||
name,
|
name,
|
||||||
@@ -78,7 +88,7 @@ test.describe("Lazy Loading", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMemberInMemberlist(page: Page, name: string): Locator {
|
function getMemberInMemberlist(page: Page, name: string): Locator {
|
||||||
return page.locator(".mx_MemberList .mx_EntityTile_name").filter({ hasText: name });
|
return page.locator(".mx_MemberListView .mx_MemberTileView_name").filter({ hasText: name });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkMemberList(page: Page, charlies: Bot[]) {
|
async function checkMemberList(page: Page, charlies: Bot[]) {
|
||||||
@@ -95,7 +105,13 @@ test.describe("Lazy Loading", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function joinCharliesWhileAliceIsOffline(page: Page, app: ElementAppPage, charlies: Bot[]) {
|
async function joinCharliesWhileAliceIsOffline(
|
||||||
|
page: Page,
|
||||||
|
app: ElementAppPage,
|
||||||
|
user: Credentials,
|
||||||
|
charlies: Bot[],
|
||||||
|
) {
|
||||||
|
const alias = `#lltest:${user.homeServer}`;
|
||||||
await app.client.network.goOffline();
|
await app.client.network.goOffline();
|
||||||
for (const charly of charlies) {
|
for (const charly of charlies) {
|
||||||
await charly.joinRoom(alias);
|
await charly.joinRoom(alias);
|
||||||
@@ -107,19 +123,19 @@ test.describe("Lazy Loading", () => {
|
|||||||
await app.client.waitForNextSync();
|
await app.client.waitForNextSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
test("should handle lazy loading properly even when offline", async ({ page, app, bot }) => {
|
test("should handle lazy loading properly even when offline", async ({ page, app, bot, user }) => {
|
||||||
test.slow();
|
test.slow();
|
||||||
const charly1to5 = charlies.slice(0, 5);
|
const charly1to5 = charlies.slice(0, 5);
|
||||||
const charly6to10 = charlies.slice(5);
|
const charly6to10 = charlies.slice(5);
|
||||||
|
|
||||||
// Set up room with alice, bob & charlies 1-5
|
// Set up room with alice, bob & charlies 1-5
|
||||||
await setupRoomWithBobAliceAndCharlies(page, app, bot, charly1to5);
|
await setupRoomWithBobAliceAndCharlies(page, app, user, bot, charly1to5);
|
||||||
// Alice should see 2 messages from every charly with the correct display name
|
// Alice should see 2 messages from every charly with the correct display name
|
||||||
await checkPaginatedDisplayNames(app, charly1to5);
|
await checkPaginatedDisplayNames(app, charly1to5);
|
||||||
|
|
||||||
await openMemberlist(app);
|
await openMemberlist(app);
|
||||||
await checkMemberList(page, charly1to5);
|
await checkMemberList(page, charly1to5);
|
||||||
await joinCharliesWhileAliceIsOffline(page, app, charly6to10);
|
await joinCharliesWhileAliceIsOffline(page, app, user, charly6to10);
|
||||||
await checkMemberList(page, charly6to10);
|
await checkMemberList(page, charly6to10);
|
||||||
|
|
||||||
for (const charly of charlies) {
|
for (const charly of charlies) {
|
||||||
|
|||||||
@@ -0,0 +1,354 @@
|
|||||||
|
/*
|
||||||
|
* 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 Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { type Locator, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import { expect, test } from "../../../element-web-test";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
test.describe("Room list filters and sort", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
botCreateOpts: {
|
||||||
|
displayName: "BotBob",
|
||||||
|
autoAcceptInvites: true,
|
||||||
|
},
|
||||||
|
labsFlags: ["feature_new_room_list"],
|
||||||
|
});
|
||||||
|
|
||||||
|
function getPrimaryFilters(page: Page): Locator {
|
||||||
|
return page.getByRole("listbox", { name: "Room list filters" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoomOptionsMenu(page: Page): Locator {
|
||||||
|
return page.getByRole("button", { name: "Room Options" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room list
|
||||||
|
* @param page
|
||||||
|
*/
|
||||||
|
function getRoomList(page: Page) {
|
||||||
|
return page.getByTestId("room-list");
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||||
|
// The notification toast is displayed above the search section
|
||||||
|
await app.closeNotificationToast();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Tombstoned rooms are not shown even when they receive updates", async ({ page, app, bot }) => {
|
||||||
|
// This bug shows up with this setting turned on
|
||||||
|
await app.settings.setValue("Spaces.allRoomsInHome", null, SettingLevel.DEVICE, true);
|
||||||
|
|
||||||
|
/*
|
||||||
|
We will first create a room named 'Old Room' and will invite the bot user to this room.
|
||||||
|
We will also send a simple message in this room.
|
||||||
|
*/
|
||||||
|
const oldRoomId = await app.client.createRoom({ name: "Old Room" });
|
||||||
|
await app.client.inviteUser(oldRoomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(oldRoomId);
|
||||||
|
const response = await app.client.sendMessage(oldRoomId, "Hello!");
|
||||||
|
|
||||||
|
/*
|
||||||
|
At this point, we haven't done anything interesting.
|
||||||
|
So we expect 'Old Room' to show up in the room list.
|
||||||
|
*/
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
const oldRoomTile = roomListView.getByRole("gridcell", { name: "Open room Old Room" });
|
||||||
|
await expect(oldRoomTile).toBeVisible();
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now let's tombstone 'Old Room'.
|
||||||
|
First we create a new room ('New Room') with the predecessor set to the old room..
|
||||||
|
*/
|
||||||
|
const newRoomId = await bot.createRoom({
|
||||||
|
name: "New Room",
|
||||||
|
creation_content: {
|
||||||
|
predecessor: {
|
||||||
|
event_id: response.event_id,
|
||||||
|
room_id: oldRoomId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visibility: "public" as Visibility,
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now we can send the tombstone event itself to the 'Old Room'.
|
||||||
|
*/
|
||||||
|
await app.client.sendStateEvent(oldRoomId, "m.room.tombstone", {
|
||||||
|
body: "This room has been replaced",
|
||||||
|
replacement_room: newRoomId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Let's join the replaced room.
|
||||||
|
await app.client.joinRoom(newRoomId);
|
||||||
|
|
||||||
|
// We expect 'Old Room' to be hidden from the room list.
|
||||||
|
await expect(oldRoomTile).not.toBeVisible();
|
||||||
|
|
||||||
|
/*
|
||||||
|
Let's say some user in the 'Old Room' changes their display name.
|
||||||
|
This will send events to the all the rooms including 'Old Room'.
|
||||||
|
Nevertheless, the replaced room should not be shown in the room list.
|
||||||
|
*/
|
||||||
|
await bot.setDisplayName("MyNewName");
|
||||||
|
await expect(oldRoomTile).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Scroll behaviour", () => {
|
||||||
|
test("should scroll to the top of list when filter is applied and active room is not in filtered list", async ({
|
||||||
|
page,
|
||||||
|
app,
|
||||||
|
}) => {
|
||||||
|
const createFavouriteRoom = async (name: string) => {
|
||||||
|
const id = await app.client.createRoom({
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
await app.client.evaluate(async (client, favouriteId) => {
|
||||||
|
await client.setRoomTag(favouriteId, "m.favourite", { order: 0.5 });
|
||||||
|
}, id);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create 5 favourite rooms
|
||||||
|
let i = 0;
|
||||||
|
for (; i < 5; i++) {
|
||||||
|
await createFavouriteRoom(`room${i}-fav`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a non-favourite room
|
||||||
|
await app.client.createRoom({ name: `room-non-fav` });
|
||||||
|
|
||||||
|
// Create rest of the favourite rooms
|
||||||
|
for (; i < 20; i++) {
|
||||||
|
await createFavouriteRoom(`room${i}-fav`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the non-favourite room
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
const tile = roomListView.getByRole("gridcell", { name: "Open room room-non-fav" });
|
||||||
|
await tile.scrollIntoViewIfNeeded();
|
||||||
|
await tile.click();
|
||||||
|
|
||||||
|
// Enable Favourite filter
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||||
|
await expect(tile).not.toBeVisible();
|
||||||
|
|
||||||
|
// Ensure the room list is not scrolled
|
||||||
|
const isScrolledDown = await page
|
||||||
|
.getByRole("grid", { name: "Room list" })
|
||||||
|
.evaluate((e) => e.scrollTop !== 0);
|
||||||
|
expect(isScrolledDown).toStrictEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Room list", () => {
|
||||||
|
let unReadDmId: string | undefined;
|
||||||
|
let unReadRoomId: string | undefined;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, app, bot, user }) => {
|
||||||
|
await app.client.createRoom({ name: "empty room" });
|
||||||
|
|
||||||
|
unReadDmId = await bot.createRoom({
|
||||||
|
name: "unread dm",
|
||||||
|
invite: [user.userId],
|
||||||
|
is_direct: true,
|
||||||
|
});
|
||||||
|
await app.client.joinRoom(unReadDmId);
|
||||||
|
await bot.sendMessage(unReadDmId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
const lowPrioId = await app.client.createRoom({ name: "Low prio room" });
|
||||||
|
await app.client.evaluate(async (client, id) => {
|
||||||
|
await client.setRoomTag(id, "m.lowpriority", { order: 0.5 });
|
||||||
|
}, lowPrioId);
|
||||||
|
|
||||||
|
await bot.createRoom({
|
||||||
|
name: "invited room",
|
||||||
|
invite: [user.userId],
|
||||||
|
is_direct: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mentionRoomId = await app.client.createRoom({ name: "room with mention" });
|
||||||
|
await app.client.inviteUser(mentionRoomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(mentionRoomId);
|
||||||
|
|
||||||
|
const clientBot = await bot.prepareClient();
|
||||||
|
await clientBot.evaluate(
|
||||||
|
async (client, { mentionRoomId, userId }) => {
|
||||||
|
await client.sendMessage(mentionRoomId, {
|
||||||
|
// @ts-ignore ignore usage of MsgType.text
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "User",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||||
|
"m.mentions": {
|
||||||
|
user_ids: [userId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ mentionRoomId, userId: user.userId },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(4);
|
||||||
|
await expect(primaryFilters).toMatchScreenshot("unread-primary-filters.png");
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Favourite" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "favourite room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "People" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "unread dm" })).toBeVisible();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(2);
|
||||||
|
|
||||||
|
await 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();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "Low prio room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(5);
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Mentions" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "room with mention" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
|
||||||
|
await primaryFilters.getByRole("option", { name: "Invites" }).click();
|
||||||
|
await expect(roomList.getByRole("gridcell", { name: "invited room" })).toBeVisible();
|
||||||
|
expect(await roomList.locator("role=gridcell").count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"unread filter should only match unread rooms that have a count",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
// Let's configure unread dm room so that we only get notification for mentions and keywords
|
||||||
|
await app.viewRoomById(unReadDmId);
|
||||||
|
await app.settings.openRoomSettings("Notifications");
|
||||||
|
await page.getByText("@mentions & keywords").click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
|
// Let's open a room other than unread room or unread dm
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room favourite room" }).click();
|
||||||
|
|
||||||
|
// Let's make the bot send a new message in both rooms
|
||||||
|
await bot.sendMessage(unReadDmId, "Hello!");
|
||||||
|
await bot.sendMessage(unReadRoomId, "Hello!");
|
||||||
|
|
||||||
|
// Let's activate the unread filter now
|
||||||
|
await page.getByRole("option", { name: "Unread" }).click();
|
||||||
|
|
||||||
|
// Unread filter should only show unread room and not unread dm!
|
||||||
|
const unreadDm = roomListView.getByRole("gridcell", { name: "Open room unread room" });
|
||||||
|
await expect(unreadDm).toBeVisible();
|
||||||
|
await expect(unreadDm).toMatchScreenshot("unread-dm.png");
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room unread dm" })).not.toBeVisible();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("should sort the room list alphabetically", async ({ page }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
await getRoomOptionsMenu(page).click();
|
||||||
|
await page.getByRole("menuitemradio", { name: "A-Z" }).click();
|
||||||
|
|
||||||
|
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/empty room/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should move room to the top on message when sorting by activity", async ({ page, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
await bot.sendMessage(unReadDmId, "Hello!");
|
||||||
|
|
||||||
|
await expect(roomListView.getByRole("gridcell").first()).toHaveText(/unread dm/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Empty room list", () => {
|
||||||
|
/**
|
||||||
|
* Get the empty state
|
||||||
|
* @param page
|
||||||
|
*/
|
||||||
|
function getEmptyRoomList(page: Page) {
|
||||||
|
return page.getByTestId("empty-room-list");
|
||||||
|
}
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should render the default placeholder when there is no filter",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot("default-empty-room-list.png");
|
||||||
|
await expect(page.getByTestId("room-list-panel")).toMatchScreenshot("room-panel-empty-room-list.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
[
|
||||||
|
{ filter: "Unreads", action: "Show all chats" },
|
||||||
|
{ filter: "Mentions", action: "See all activity" },
|
||||||
|
{ filter: "Invites", action: "See all activity" },
|
||||||
|
].forEach(({ filter, action }) => {
|
||||||
|
test(
|
||||||
|
`should render the placeholder for ${filter} filter`,
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||||
|
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||||
|
|
||||||
|
await emptyRoomList.getByRole("button", { name: action }).click();
|
||||||
|
await expect(primaryFilters.getByRole("option", { name: filter })).not.toBeChecked();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
["People", "Rooms", "Favourite"].forEach((filter) => {
|
||||||
|
test(
|
||||||
|
`should render the placeholder for ${filter} filter`,
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const primaryFilters = getPrimaryFilters(page);
|
||||||
|
await primaryFilters.getByRole("option", { name: filter }).click();
|
||||||
|
|
||||||
|
const emptyRoomList = getEmptyRoomList(page);
|
||||||
|
await expect(emptyRoomList).toMatchScreenshot(`${filter}-empty-room-list.png`);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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 panel", () => {
|
||||||
|
test.use({
|
||||||
|
labsFlags: ["feature_new_room_list"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the room list view
|
||||||
|
* @param page
|
||||||
|
*/
|
||||||
|
function getRoomListView(page: Page) {
|
||||||
|
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}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the room list panel", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomListView(page);
|
||||||
|
// 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");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should respond to small screen sizes", { tag: "@screenshot" }, async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 575, height: 600 });
|
||||||
|
const roomListPanel = page.getByTestId("room-list-panel");
|
||||||
|
await expect(roomListPanel).toMatchScreenshot("room-list-panel-smallscreen.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* 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("Search section of the room list", () => {
|
||||||
|
test.use({
|
||||||
|
labsFlags: ["feature_new_room_list"],
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the search section of the room list
|
||||||
|
* @param page
|
||||||
|
*/
|
||||||
|
function getSearchSection(page: Page) {
|
||||||
|
return page.getByRole("search");
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, app, user }) => {
|
||||||
|
// The notification toast is displayed above the search section
|
||||||
|
await app.closeNotificationToast();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the search section", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
const searchSection = getSearchSection(page);
|
||||||
|
// exact=false to ignore the shortcut which is related to the OS
|
||||||
|
await expect(searchSection.getByRole("button", { name: "Search", exact: false })).toBeVisible();
|
||||||
|
await expect(searchSection).toMatchScreenshot("search-section.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open the spotlight when the search button is clicked", async ({ page, app, user }) => {
|
||||||
|
const searchSection = getSearchSection(page);
|
||||||
|
await searchSection.getByRole("button", { name: "Search", exact: false }).click();
|
||||||
|
// The spotlight should be displayed
|
||||||
|
await expect(page.getByRole("dialog", { name: "Search Dialog" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open the room directory when the search button is clicked", async ({ page, app, user }) => {
|
||||||
|
const searchSection = getSearchSection(page);
|
||||||
|
await searchSection.getByRole("button", { name: "Explore rooms" }).click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Search Dialog" });
|
||||||
|
// The room directory should be displayed
|
||||||
|
await expect(dialog).toBeVisible();
|
||||||
|
// The public room filter should be displayed
|
||||||
|
await expect(dialog.getByText("Public rooms")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
417
playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
Normal file
417
playwright/e2e/left-panel/room-list-panel/room-list.spec.ts
Normal file
@@ -0,0 +1,417 @@
|
|||||||
|
/*
|
||||||
|
* 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 { expect, test } from "../../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Room list", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
labsFlags: ["feature_new_room_list"],
|
||||||
|
botCreateOpts: {
|
||||||
|
displayName: "BotBob",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Room list", () => {
|
||||||
|
test.beforeEach(async ({ page, app, user }) => {
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Put focus on the room list
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
|
// Scroll to the end of the room list
|
||||||
|
await page.mouse.wheel(0, 1000);
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
|
await 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();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should open the notification 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");
|
||||||
|
let roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||||
|
await roomItemMenu.click();
|
||||||
|
|
||||||
|
// Default settings should be selected
|
||||||
|
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
await expect(page).toMatchScreenshot("room-list-item-open-notification-options.png");
|
||||||
|
|
||||||
|
// It should make the room muted
|
||||||
|
await page.getByRole("menuitem", { name: "Mute room" }).click();
|
||||||
|
|
||||||
|
// Put focus on the room list
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||||
|
// Scroll to the end of the room list
|
||||||
|
await page.mouse.wheel(0, 1000);
|
||||||
|
|
||||||
|
// The room decoration should have the muted icon
|
||||||
|
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||||
|
|
||||||
|
await roomItem.hover();
|
||||||
|
// On hover, the room should show the muted icon
|
||||||
|
await expect(roomItem).toMatchScreenshot("room-list-item-hover-silent.png");
|
||||||
|
|
||||||
|
roomItemMenu = roomItem.getByRole("button", { name: "Notification options" });
|
||||||
|
await roomItemMenu.click();
|
||||||
|
// The Mute room option should be selected
|
||||||
|
await expect(page.getByRole("menuitem", { name: "Mute room" })).toHaveAttribute("aria-selected", "true");
|
||||||
|
await expect(page).toMatchScreenshot("room-list-item-open-notification-options-selection.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should scroll to the current room", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
// Put focus on the room list
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
|
// Scroll to the end of the room list
|
||||||
|
await page.mouse.wheel(0, 1000);
|
||||||
|
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||||
|
|
||||||
|
const filters = page.getByRole("listbox", { name: "Room list filters" });
|
||||||
|
await filters.getByRole("option", { name: "People" }).click();
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).not.toBeVisible();
|
||||||
|
|
||||||
|
await filters.getByRole("option", { name: "People" }).click();
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Shortcuts", () => {
|
||||||
|
test("should select the next room", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
|
await page.keyboard.press("Alt+ArrowDown");
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "room28", level: 1 })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should select the previous room", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||||
|
await page.keyboard.press("Alt+ArrowUp");
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "room29", level: 1 })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should select the last room", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
|
await page.keyboard.press("Alt+ArrowUp");
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "room0", level: 1 })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should select the next unread room", async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "1 notification" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
await roomListView.getByRole("gridcell", { name: "Open room room20" }).click();
|
||||||
|
|
||||||
|
await page.keyboard.press("Alt+Shift+ArrowDown");
|
||||||
|
|
||||||
|
await expect(page.getByRole("heading", { name: "1 notification", level: 1 })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Keyboard navigation", () => {
|
||||||
|
test("should navigate to the room list", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
|
const room28 = roomListView.getByRole("gridcell", { name: "Open room room28" });
|
||||||
|
|
||||||
|
// open the room
|
||||||
|
await room29.click();
|
||||||
|
// put focus back on the room list item
|
||||||
|
await room29.click();
|
||||||
|
await expect(room29).toBeFocused();
|
||||||
|
|
||||||
|
await page.keyboard.press("ArrowDown");
|
||||||
|
await expect(room28).toBeFocused();
|
||||||
|
await expect(room29).not.toBeFocused();
|
||||||
|
|
||||||
|
await page.keyboard.press("ArrowUp");
|
||||||
|
await expect(room29).toBeFocused();
|
||||||
|
await expect(room28).not.toBeFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should navigate to the notification menu", async ({ page, app, user }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
const room29 = roomListView.getByRole("gridcell", { name: "Open room room29" });
|
||||||
|
const moreButton = room29.getByRole("button", { name: "More options" });
|
||||||
|
const notificationButton = room29.getByRole("button", { name: "Notification options" });
|
||||||
|
|
||||||
|
await room29.click();
|
||||||
|
// put focus back on the room list item
|
||||||
|
await room29.click();
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
await expect(moreButton).toBeFocused();
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
await expect(notificationButton).toBeFocused();
|
||||||
|
|
||||||
|
// Open the menu
|
||||||
|
await notificationButton.click();
|
||||||
|
// Wait for the menu to be open
|
||||||
|
await expect(page.getByRole("menuitem", { name: "Match default settings" })).toHaveAttribute(
|
||||||
|
"aria-selected",
|
||||||
|
"true",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close the menu
|
||||||
|
await page.keyboard.press("Escape");
|
||||||
|
// Focus should be back on the room list item
|
||||||
|
await expect(room29).toBeFocused();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Avatar decoration", () => {
|
||||||
|
test.use({ labsFlags: ["feature_video_rooms", "feature_new_room_list"] });
|
||||||
|
|
||||||
|
test("should be a public room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
// @ts-ignore Visibility enum is not accessible
|
||||||
|
await app.client.createRoom({ name: "public room", visibility: "public" });
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
const publicRoom = roomListView.getByRole("gridcell", { name: "public room" });
|
||||||
|
|
||||||
|
await expect(publicRoom).toBeVisible();
|
||||||
|
await expect(publicRoom).toMatchScreenshot("room-list-item-public.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be a video room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
await page.getByTestId("room-list-panel").getByRole("button", { name: "Add" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "New video room" }).click();
|
||||||
|
await page.getByRole("textbox", { name: "Name" }).fill("video room");
|
||||||
|
await page.getByRole("button", { name: "Create video room" }).click();
|
||||||
|
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
const videoRoom = roomListView.getByRole("gridcell", { name: "video room" });
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
|
||||||
|
await expect(videoRoom).toBeVisible();
|
||||||
|
await expect(videoRoom).toMatchScreenshot("room-list-item-video.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Notification decoration", () => {
|
||||||
|
test("should render the invitation decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
await bot.createRoom({
|
||||||
|
name: "invited room",
|
||||||
|
invite: [user.userId],
|
||||||
|
is_direct: true,
|
||||||
|
});
|
||||||
|
const invitedRoom = roomListView.getByRole("gridcell", { name: "invited room" });
|
||||||
|
await expect(invitedRoom).toBeVisible();
|
||||||
|
await expect(invitedRoom).toMatchScreenshot("room-list-item-invited.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the regular decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "2 notifications" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "2 notifications" });
|
||||||
|
await expect(room).toBeVisible();
|
||||||
|
await expect(room.getByTestId("notification-decoration")).toHaveText("2");
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-notification.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the mention decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "mention" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
const clientBot = await bot.prepareClient();
|
||||||
|
await clientBot.evaluate(
|
||||||
|
async (client, { roomId, userId }) => {
|
||||||
|
await client.sendMessage(roomId, {
|
||||||
|
// @ts-ignore ignore usage of MsgType.text
|
||||||
|
"msgtype": "m.text",
|
||||||
|
"body": "User",
|
||||||
|
"format": "org.matrix.custom.html",
|
||||||
|
"formatted_body": `<a href="https://matrix.to/#/${userId}">User</a>`,
|
||||||
|
"m.mentions": {
|
||||||
|
user_ids: [userId],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ roomId, userId: user.userId },
|
||||||
|
);
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "mention" });
|
||||||
|
await expect(room).toBeVisible();
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-mention.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render a message preview", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Room Options" }).click();
|
||||||
|
await page.getByRole("menuitemcheckbox", { name: "Show message previews" }).click();
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "activity" });
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||||
|
await expect(room.getByText("I am a robot. Beep.")).toBeVisible();
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-message-preview.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render an activity decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const otherRoomId = await app.client.createRoom({ name: "other room" });
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "activity" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
await app.viewRoomById(roomId);
|
||||||
|
await app.settings.openRoomSettings("Notifications");
|
||||||
|
await page.getByText("@mentions & keywords").click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
|
await app.settings.openUserSettings("Notifications");
|
||||||
|
await page.getByText("Show all activity in the room list (dots or number of unread messages)").click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
|
// Switch to the other room to avoid the notification to be cleared
|
||||||
|
await app.viewRoomById(otherRoomId);
|
||||||
|
await bot.sendMessage(roomId, "I am a robot. Beep.");
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "activity" });
|
||||||
|
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-activity.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render a mark as unread decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "mark as unread" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "mark as unread" });
|
||||||
|
await room.hover();
|
||||||
|
await room.getByRole("button", { name: "More Options" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "mark as unread" }).click();
|
||||||
|
|
||||||
|
// focus the user menu to avoid to have hover decoration
|
||||||
|
await page.getByRole("button", { name: "User menu" }).focus();
|
||||||
|
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-mark-as-unread.png");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render silent decoration", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
const roomListView = getRoomList(page);
|
||||||
|
|
||||||
|
const roomId = await app.client.createRoom({ name: "silent" });
|
||||||
|
await app.client.inviteUser(roomId, bot.credentials.userId);
|
||||||
|
await bot.joinRoom(roomId);
|
||||||
|
|
||||||
|
await app.viewRoomById(roomId);
|
||||||
|
await app.settings.openRoomSettings("Notifications");
|
||||||
|
await page.getByText("Off").click();
|
||||||
|
await app.settings.closeDialog();
|
||||||
|
|
||||||
|
const room = roomListView.getByRole("gridcell", { name: "silent" });
|
||||||
|
await expect(room.getByTestId("notification-decoration")).toBeVisible();
|
||||||
|
await expect(room).toMatchScreenshot("room-list-item-silent.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Locator, Page } from "@playwright/test";
|
import { type Locator, type Page } from "@playwright/test";
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../element-web-test";
|
||||||
|
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||||
|
|
||||||
test.describe("Consent", () => {
|
test.use(consentHomeserver);
|
||||||
test.use({
|
test.use({
|
||||||
startHomeserverOpts: "consent",
|
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe("Consent", () => {
|
||||||
test("should prompt the user to consent to terms when server deems it necessary", async ({
|
test("should prompt the user to consent to terms when server deems it necessary", async ({
|
||||||
context,
|
context,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ 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.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Page } from "playwright-core";
|
import { type Page } from "@playwright/test";
|
||||||
|
|
||||||
import { expect, test } from "../../element-web-test";
|
import { expect, test } from "../../element-web-test";
|
||||||
import { doTokenRegistration } from "./utils";
|
|
||||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
|
||||||
import { selectHomeserver } from "../utils";
|
import { selectHomeserver } from "../utils";
|
||||||
import { Credentials, HomeserverInstance } from "../../plugins/homeserver";
|
import { type Credentials, type HomeserverInstance } from "../../plugins/homeserver";
|
||||||
|
import { consentHomeserver } from "../../plugins/homeserver/synapse/consentHomeserver.ts";
|
||||||
|
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||||
|
import { createBot } from "../crypto/utils.ts";
|
||||||
|
|
||||||
|
// This test requires fixed credentials for the device signing keys below to work
|
||||||
const username = "user1234";
|
const username = "user1234";
|
||||||
const password = "p4s5W0rD";
|
const password = "p4s5W0rD";
|
||||||
|
|
||||||
@@ -68,29 +70,58 @@ const DEVICE_SIGNING_KEYS_BODY = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function login(page: Page, homeserver: HomeserverInstance) {
|
async function login(page: Page, homeserver: HomeserverInstance, credentials: Credentials) {
|
||||||
await page.getByRole("link", { name: "Sign in" }).click();
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
await selectHomeserver(page, homeserver.baseUrl);
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||||
await page.getByPlaceholder("Password").fill(password);
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
await page.getByRole("button", { name: "Sign in" }).click();
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe("Login", () => {
|
// This test suite uses the same userId for all tests in the suite
|
||||||
test.describe("Password login", () => {
|
// due to DEVICE_SIGNING_KEYS_BODY being specific to that userId,
|
||||||
test.use({ startHomeserverOpts: "consent" });
|
// so we restart the Synapse container to make it forget everything.
|
||||||
|
test.use(consentHomeserver);
|
||||||
|
test.use({
|
||||||
|
config: {
|
||||||
|
// The only thing that we really *need* (otherwise Element refuses to load) is a default homeserver.
|
||||||
|
// We point that to a guaranteed-invalid domain.
|
||||||
|
default_server_config: {
|
||||||
|
"m.homeserver": {
|
||||||
|
base_url: "https://server.invalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
context: async ({ context, homeserver }, use) => {
|
||||||
|
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||||
|
await homeserver.restart();
|
||||||
|
await use(context);
|
||||||
|
},
|
||||||
|
credentials: async ({ context, homeserver }, use) => {
|
||||||
|
const displayName = "Dave";
|
||||||
|
const credentials = await homeserver.registerUser(username, password, displayName);
|
||||||
|
console.log(`Registered test user @user:localhost with displayname ${displayName}`);
|
||||||
|
|
||||||
let creds: Credentials;
|
await use({
|
||||||
|
...credentials,
|
||||||
test.beforeEach(async ({ homeserver }) => {
|
displayName,
|
||||||
creds = await homeserver.registerUser(username, password);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restart the homeserver to wipe its in-memory db so we can reuse the same user ID without cross-signing prompts
|
||||||
|
await homeserver.restart();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Login", () => {
|
||||||
|
test.describe("Password login", () => {
|
||||||
|
test.skip(isDendrite, "Dendrite lacks support for MSC3967 so requires additional auth here");
|
||||||
|
|
||||||
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
|
test("Loads the welcome page by default; then logs in with an existing account and lands on the home screen", async ({
|
||||||
|
credentials,
|
||||||
page,
|
page,
|
||||||
homeserver,
|
homeserver,
|
||||||
checkA11y,
|
axe,
|
||||||
}) => {
|
}) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
@@ -101,7 +132,7 @@ test.describe("Login", () => {
|
|||||||
await page.getByRole("link", { name: "Sign in" }).click();
|
await page.getByRole("link", { name: "Sign in" }).click();
|
||||||
|
|
||||||
// first pick the homeserver, as otherwise the user picker won't be visible
|
// first pick the homeserver, as otherwise the user picker won't be visible
|
||||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
await selectHomeserver(page, homeserver.baseUrl);
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Edit" }).click();
|
await page.getByRole("button", { name: "Edit" }).click();
|
||||||
|
|
||||||
@@ -114,23 +145,23 @@ test.describe("Login", () => {
|
|||||||
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
|
await expect(page.locator(".mx_ServerPicker_server")).toHaveText("server.invalid");
|
||||||
|
|
||||||
// switch back to the custom homeserver
|
// switch back to the custom homeserver
|
||||||
await selectHomeserver(page, homeserver.config.baseUrl);
|
await selectHomeserver(page, homeserver.baseUrl);
|
||||||
|
|
||||||
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
await expect(page.getByRole("textbox", { name: "Username" })).toBeVisible();
|
||||||
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
|
// Disabled because flaky - see https://github.com/vector-im/element-web/issues/24688
|
||||||
// cy.percySnapshot("Login");
|
// cy.percySnapshot("Login");
|
||||||
await checkA11y();
|
await expect(axe).toHaveNoViolations();
|
||||||
|
|
||||||
await page.getByRole("textbox", { name: "Username" }).fill(username);
|
await page.getByRole("textbox", { name: "Username" }).fill(credentials.username);
|
||||||
await page.getByPlaceholder("Password").fill(password);
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
await page.getByRole("button", { name: "Sign in" }).click();
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/#\/home$/);
|
await expect(page).toHaveURL(/\/#\/home$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Follows the original link after login", async ({ page, homeserver }) => {
|
test("Follows the original link after login", async ({ page, homeserver, credentials }) => {
|
||||||
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
|
await page.goto("/#/room/!room:id"); // should redirect to the welcome page
|
||||||
await login(page, homeserver);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
await expect(page).toHaveURL(/\/#\/room\/!room:id$/);
|
||||||
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
await expect(page.getByRole("button", { name: "Join the discussion" })).toBeVisible();
|
||||||
@@ -141,18 +172,19 @@ test.describe("Login", () => {
|
|||||||
page,
|
page,
|
||||||
homeserver,
|
homeserver,
|
||||||
request,
|
request,
|
||||||
|
credentials,
|
||||||
}) => {
|
}) => {
|
||||||
const res = await request.post(
|
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
|
||||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
data: DEVICE_SIGNING_KEYS_BODY,
|
||||||
);
|
});
|
||||||
if (res.status() / 100 !== 2) {
|
if (res.status() / 100 !== 2) {
|
||||||
console.log("Uploading dummy keys failed", await res.json());
|
console.log("Uploading dummy keys failed", await res.json());
|
||||||
}
|
}
|
||||||
expect(res.status() / 100).toEqual(2);
|
expect(res.status() / 100).toEqual(2);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
|
|
||||||
@@ -170,10 +202,14 @@ test.describe("Login", () => {
|
|||||||
page,
|
page,
|
||||||
homeserver,
|
homeserver,
|
||||||
request,
|
request,
|
||||||
|
credentials,
|
||||||
}) => {
|
}) => {
|
||||||
const res = await request.post(
|
const res = await request.post(
|
||||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
{
|
||||||
|
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||||
|
data: DEVICE_SIGNING_KEYS_BODY,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (res.status() / 100 !== 2) {
|
if (res.status() / 100 !== 2) {
|
||||||
console.log("Uploading dummy keys failed", await res.json());
|
console.log("Uploading dummy keys failed", await res.json());
|
||||||
@@ -181,7 +217,7 @@ test.describe("Login", () => {
|
|||||||
expect(res.status() / 100).toEqual(2);
|
expect(res.status() / 100).toEqual(2);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
|
|
||||||
@@ -200,11 +236,15 @@ test.describe("Login", () => {
|
|||||||
page,
|
page,
|
||||||
homeserver,
|
homeserver,
|
||||||
request,
|
request,
|
||||||
|
credentials,
|
||||||
}) => {
|
}) => {
|
||||||
console.log(`uid ${creds.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
console.log(`uid ${credentials.userId} body`, DEVICE_SIGNING_KEYS_BODY);
|
||||||
const res = await request.post(
|
const res = await request.post(
|
||||||
`${homeserver.config.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`,
|
||||||
{ headers: { Authorization: `Bearer ${creds.accessToken}` }, data: DEVICE_SIGNING_KEYS_BODY },
|
{
|
||||||
|
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||||
|
data: DEVICE_SIGNING_KEYS_BODY,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (res.status() / 100 !== 2) {
|
if (res.status() / 100 !== 2) {
|
||||||
console.log("Uploading dummy keys failed", await res.json());
|
console.log("Uploading dummy keys failed", await res.json());
|
||||||
@@ -212,43 +252,83 @@ test.describe("Login", () => {
|
|||||||
expect(res.status() / 100).toEqual(2);
|
expect(res.status() / 100).toEqual(2);
|
||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await login(page, homeserver);
|
await login(page, homeserver, credentials);
|
||||||
|
|
||||||
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
|
const h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
await expect(h1).toBeVisible();
|
await expect(h1).toBeVisible();
|
||||||
|
|
||||||
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
test("Continues to show verification prompt after cancelling device verification", async ({
|
||||||
|
page,
|
||||||
|
homeserver,
|
||||||
|
credentials,
|
||||||
|
}) => {
|
||||||
|
// Create a different device which is cross-signed, meaning we need to verify this device
|
||||||
|
await createBot(page, homeserver, credentials, true);
|
||||||
|
|
||||||
|
// Wait to avoid homeserver rate limit on logins
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
|
||||||
|
// Load the page and see that we are asked to verify
|
||||||
|
await page.goto("/#/welcome");
|
||||||
|
await login(page, homeserver, credentials);
|
||||||
|
let h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
|
await expect(h1).toBeVisible();
|
||||||
|
|
||||||
|
// Click "Verify with another device"
|
||||||
|
await page.getByRole("button", { name: "Verify with another device" }).click();
|
||||||
|
|
||||||
|
// Cancel the new dialog
|
||||||
|
await page.getByRole("button", { name: "Close dialog" }).click();
|
||||||
|
|
||||||
|
// Check that we are still being asked to verify
|
||||||
|
h1 = page.getByRole("heading", { name: "Verify this device", level: 1 });
|
||||||
|
await expect(h1).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// tests for old-style SSO login, in which we exchange tokens with Synapse, and Synapse talks to an auth server
|
test("Can reset identity to become verified", async ({ page, homeserver, request, credentials }) => {
|
||||||
test.describe("SSO login", () => {
|
// Log in
|
||||||
test.skip(isDendrite, "does not yet support SSO");
|
const res = await request.post(`${homeserver.baseUrl}/_matrix/client/v3/keys/device_signing/upload`, {
|
||||||
|
headers: { Authorization: `Bearer ${credentials.accessToken}` },
|
||||||
test.use({
|
data: DEVICE_SIGNING_KEYS_BODY,
|
||||||
startHomeserverOpts: ({ oAuthServer }, use) =>
|
|
||||||
use({
|
|
||||||
template: "default",
|
|
||||||
oAuthServerPort: oAuthServer.port,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
if (!res.ok()) {
|
||||||
|
console.log(`Uploading dummy keys failed with HTTP status ${res.status}`, await res.json());
|
||||||
|
throw new Error("Uploading dummy keys failed");
|
||||||
|
}
|
||||||
|
|
||||||
test("logs in with SSO and lands on the home screen", async ({ page, homeserver }) => {
|
await page.goto("/");
|
||||||
// If this test fails with a screen showing "Timeout connecting to remote server", it is most likely due to
|
await login(page, homeserver, credentials);
|
||||||
// your firewall settings: Synapse is unable to reach the OIDC server.
|
|
||||||
//
|
await expect(page.getByRole("heading", { name: "Verify this device", level: 1 })).toBeVisible();
|
||||||
// If you are using ufw, try something like:
|
|
||||||
// sudo ufw allow in on docker0
|
// Start the reset process
|
||||||
//
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
await doTokenRegistration(page, homeserver);
|
|
||||||
|
// First try cancelling and restarting
|
||||||
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
|
|
||||||
|
// Then click outside the dialog and restart
|
||||||
|
await page.getByRole("link", { name: "Powered by Matrix" }).click({ force: true });
|
||||||
|
await page.getByRole("button", { name: "Proceed with reset" }).click();
|
||||||
|
|
||||||
|
// Finally we actually continue
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await page.getByPlaceholder("Password").fill(credentials.password);
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
|
||||||
|
// We end up at the Home screen
|
||||||
|
await expect(page).toHaveURL(/\/#\/home$/, { timeout: 10000 });
|
||||||
|
await expect(page.getByRole("heading", { name: "Welcome Dave", exact: true })).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("logout", () => {
|
test.describe("logout", () => {
|
||||||
test.use({ startHomeserverOpts: "consent" });
|
|
||||||
|
|
||||||
test("should go to login page on logout", async ({ page, user }) => {
|
test("should go to login page on logout", async ({ page, user }) => {
|
||||||
await page.getByRole("button", { name: "User menu" }).click();
|
await page.getByRole("button", { name: "User menu" }).click();
|
||||||
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
||||||
@@ -260,29 +340,4 @@ test.describe("Login", () => {
|
|||||||
await expect(page).toHaveURL(/\/#\/login$/);
|
await expect(page).toHaveURL(/\/#\/login$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("logout with logout_redirect_url", () => {
|
|
||||||
test.use({
|
|
||||||
startHomeserverOpts: "consent",
|
|
||||||
config: {
|
|
||||||
// We redirect to decoder-ring because it's a predictable page that isn't Element itself.
|
|
||||||
// We could use example.org, matrix.org, or something else, however this puts dependency of external
|
|
||||||
// infrastructure on our tests. In the same vein, we don't really want to figure out how to ship a
|
|
||||||
// `test-landing.html` page when running with an uncontrolled Element (via `yarn start`).
|
|
||||||
// Using the decoder-ring is just as fine, and we can search for strategic names.
|
|
||||||
logout_redirect_url: "/decoder-ring/",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should respect logout_redirect_url", async ({ page, user }) => {
|
|
||||||
await page.getByRole("button", { name: "User menu" }).click();
|
|
||||||
await expect(page.getByText(user.displayName, { exact: true })).toBeVisible();
|
|
||||||
|
|
||||||
// give a change for the outstanding requests queue to settle before logging out
|
|
||||||
await page.waitForTimeout(2000);
|
|
||||||
|
|
||||||
await page.locator(".mx_UserMenu_contextMenu").getByRole("menuitem", { name: "Sign out" }).click();
|
|
||||||
await expect(page).toHaveURL(/\/decoder-ring\/$/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user