@element-hq/element-web-playwright-common 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/Dockerfile +9 -0
  2. package/README.md +28 -0
  3. package/lib/element-web-test.d.ts +26 -0
  4. package/lib/element-web-test.d.ts.map +1 -0
  5. package/lib/element-web-test.js +54 -0
  6. package/lib/expect/axe.d.ts +11 -0
  7. package/lib/expect/axe.d.ts.map +1 -0
  8. package/lib/expect/axe.js +22 -0
  9. package/lib/expect/index.d.ts +6 -0
  10. package/lib/expect/index.d.ts.map +1 -0
  11. package/lib/expect/index.js +10 -0
  12. package/lib/expect/screenshot.d.ts +14 -0
  13. package/lib/expect/screenshot.d.ts.map +1 -0
  14. package/lib/expect/screenshot.js +48 -0
  15. package/lib/expect/toHaveNoViolations.d.ts +11 -0
  16. package/lib/expect/toHaveNoViolations.d.ts.map +1 -0
  17. package/lib/expect/toHaveNoViolations.js +22 -0
  18. package/lib/expect/toMatchScreenshot.d.ts +13 -0
  19. package/lib/expect/toMatchScreenshot.d.ts.map +1 -0
  20. package/lib/expect/toMatchScreenshot.js +44 -0
  21. package/lib/expect/toPassAxeCheck.d.ts +7 -0
  22. package/lib/expect/toPassAxeCheck.d.ts.map +1 -0
  23. package/lib/expect/toPassAxeCheck.js +22 -0
  24. package/lib/fixtures/axe.d.ts +8 -0
  25. package/lib/fixtures/axe.d.ts.map +1 -0
  26. package/lib/fixtures/axe.js +15 -0
  27. package/lib/fixtures/index.d.ts +10 -0
  28. package/lib/fixtures/index.d.ts.map +1 -0
  29. package/lib/fixtures/index.js +10 -0
  30. package/lib/fixtures/services.d.ts +57 -0
  31. package/lib/fixtures/services.d.ts.map +1 -0
  32. package/lib/fixtures/services.js +114 -0
  33. package/lib/fixtures/user.d.ts +31 -0
  34. package/lib/fixtures/user.d.ts.map +1 -0
  35. package/lib/fixtures/user.js +47 -0
  36. package/lib/index.d.ts +35 -0
  37. package/lib/index.d.ts.map +1 -0
  38. package/lib/index.js +54 -0
  39. package/lib/logger.d.ts +10 -0
  40. package/lib/logger.d.ts.map +1 -0
  41. package/lib/logger.js +59 -0
  42. package/lib/playwright.d.ts +2 -0
  43. package/lib/playwright.d.ts.map +1 -0
  44. package/lib/playwright.js +1 -0
  45. package/lib/testcontainers/HomeserverContainer.d.ts +70 -0
  46. package/lib/testcontainers/HomeserverContainer.d.ts.map +1 -0
  47. package/lib/testcontainers/HomeserverContainer.js +7 -0
  48. package/lib/testcontainers/index.d.ts +5 -0
  49. package/lib/testcontainers/index.d.ts.map +1 -0
  50. package/lib/testcontainers/index.js +9 -0
  51. package/lib/testcontainers/mailpit.d.ts +33 -0
  52. package/lib/testcontainers/mailpit.d.ts.map +1 -0
  53. package/lib/testcontainers/mailpit.js +52 -0
  54. package/lib/testcontainers/mas.d.ts +182 -0
  55. package/lib/testcontainers/mas.d.ts.map +1 -0
  56. package/lib/testcontainers/mas.js +311 -0
  57. package/lib/testcontainers/synapse.d.ts +229 -0
  58. package/lib/testcontainers/synapse.d.ts.map +1 -0
  59. package/lib/testcontainers/synapse.js +383 -0
  60. package/lib/utils/api.d.ts +49 -0
  61. package/lib/utils/api.d.ts.map +1 -0
  62. package/lib/utils/api.js +73 -0
  63. package/lib/utils/logger.d.ts +25 -0
  64. package/lib/utils/logger.d.ts.map +1 -0
  65. package/lib/utils/logger.js +74 -0
  66. package/lib/utils/object.d.ts +8 -0
  67. package/lib/utils/object.d.ts.map +1 -0
  68. package/lib/utils/object.js +15 -0
  69. package/lib/utils/port.d.ts +5 -0
  70. package/lib/utils/port.d.ts.map +1 -0
  71. package/lib/utils/port.js +20 -0
  72. package/lib/utils/rand.d.ts +6 -0
  73. package/lib/utils/rand.d.ts.map +1 -0
  74. package/lib/utils/rand.js +15 -0
  75. package/package.json +30 -0
  76. package/playwright-screenshots.sh +53 -0
  77. package/src/@types/playwright-core.d.ts +12 -0
  78. package/src/expect/axe.ts +37 -0
  79. package/src/expect/index.ts +21 -0
  80. package/src/expect/screenshot.ts +79 -0
  81. package/src/fixtures/axe.ts +22 -0
  82. package/src/fixtures/index.ts +15 -0
  83. package/src/fixtures/services.ts +188 -0
  84. package/src/fixtures/user.ts +93 -0
  85. package/src/index.ts +92 -0
  86. package/src/testcontainers/HomeserverContainer.ts +87 -0
  87. package/src/testcontainers/index.ts +15 -0
  88. package/src/testcontainers/mailpit.ts +62 -0
  89. package/src/testcontainers/mas.ts +382 -0
  90. package/src/testcontainers/synapse.ts +493 -0
  91. package/src/utils/api.ts +113 -0
  92. package/src/utils/logger.ts +79 -0
  93. package/src/utils/object.ts +16 -0
  94. package/src/utils/port.ts +22 -0
  95. package/src/utils/rand.ts +17 -0
  96. package/tsconfig.json +10 -0
@@ -0,0 +1,383 @@
1
+ /*
2
+ Copyright 2024 New Vector Ltd.
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
+ Please see LICENSE files in the repository root for full details.
6
+ */
7
+ import { AbstractStartedContainer, GenericContainer, Wait, } from "testcontainers";
8
+ import crypto from "node:crypto";
9
+ import * as YAML from "yaml";
10
+ import { set } from "lodash-es";
11
+ import { getFreePort } from "../utils/port.js";
12
+ import { randB64Bytes } from "../utils/rand.js";
13
+ import { deepCopy } from "../utils/object.js";
14
+ import { Api, ClientServerApi } from "../utils/api.js";
15
+ const TAG = "develop@sha256:8d0049e8e0524ad6817cf7737453fe47de1ed3b8d04704f0c2fd6c136414c9d7";
16
+ const DEFAULT_CONFIG = {
17
+ server_name: "localhost",
18
+ public_baseurl: "", // set by start method
19
+ pid_file: "/homeserver.pid",
20
+ web_client: false,
21
+ soft_file_limit: 0,
22
+ // Needs to be configured to log to the console like a good docker process
23
+ log_config: "/data/log.config",
24
+ listeners: [
25
+ {
26
+ // Listener is always port 8008 (configured in the container)
27
+ port: 8008,
28
+ tls: false,
29
+ bind_addresses: ["::"],
30
+ type: "http",
31
+ x_forwarded: true,
32
+ resources: [
33
+ {
34
+ names: ["client"],
35
+ compress: false,
36
+ },
37
+ ],
38
+ },
39
+ ],
40
+ database: {
41
+ // An sqlite in-memory database is fast & automatically wipes each time
42
+ name: "sqlite3",
43
+ args: {
44
+ database: ":memory:",
45
+ },
46
+ },
47
+ rc_messages_per_second: 10000,
48
+ rc_message_burst_count: 10000,
49
+ rc_registration: {
50
+ per_second: 10000,
51
+ burst_count: 10000,
52
+ },
53
+ rc_joins: {
54
+ local: {
55
+ per_second: 9999,
56
+ burst_count: 9999,
57
+ },
58
+ remote: {
59
+ per_second: 9999,
60
+ burst_count: 9999,
61
+ },
62
+ },
63
+ rc_joins_per_room: {
64
+ per_second: 9999,
65
+ burst_count: 9999,
66
+ },
67
+ rc_3pid_validation: {
68
+ per_second: 1000,
69
+ burst_count: 1000,
70
+ },
71
+ rc_invites: {
72
+ per_room: {
73
+ per_second: 1000,
74
+ burst_count: 1000,
75
+ },
76
+ per_user: {
77
+ per_second: 1000,
78
+ burst_count: 1000,
79
+ },
80
+ },
81
+ rc_login: {
82
+ address: {
83
+ per_second: 10000,
84
+ burst_count: 10000,
85
+ },
86
+ account: {
87
+ per_second: 10000,
88
+ burst_count: 10000,
89
+ },
90
+ failed_attempts: {
91
+ per_second: 10000,
92
+ burst_count: 10000,
93
+ },
94
+ },
95
+ media_store_path: "/tmp/media_store",
96
+ max_upload_size: "50M",
97
+ max_image_pixels: "32M",
98
+ dynamic_thumbnails: false,
99
+ enable_registration: true,
100
+ enable_registration_without_verification: true,
101
+ disable_msisdn_registration: false,
102
+ registrations_require_3pid: [],
103
+ enable_metrics: false,
104
+ report_stats: false,
105
+ // These placeholders will be replaced with values generated at start
106
+ registration_shared_secret: "secret",
107
+ macaroon_secret_key: "secret",
108
+ form_secret: "secret",
109
+ // Signing key must be here: it will be generated to this file
110
+ signing_key_path: "/data/localhost.signing.key",
111
+ trusted_key_servers: [],
112
+ password_config: {
113
+ enabled: true,
114
+ },
115
+ ui_auth: {},
116
+ background_updates: {
117
+ // Inhibit background updates as this Synapse isn't long-lived
118
+ min_batch_size: 100000,
119
+ sleep_duration_ms: 100000,
120
+ },
121
+ enable_authenticated_media: true,
122
+ email: undefined,
123
+ user_consent: undefined,
124
+ server_notices: undefined,
125
+ allow_guest_access: false,
126
+ experimental_features: {},
127
+ oidc_providers: [],
128
+ serve_server_wellknown: true,
129
+ presence: {
130
+ enabled: true,
131
+ include_offline_users_on_sync: true,
132
+ },
133
+ room_list_publication_rules: [{ action: "allow" }],
134
+ };
135
+ /**
136
+ * A Synapse testcontainer
137
+ *
138
+ * Exposes port 8008.
139
+ * Waits for HTTP /health 8008 to 200.
140
+ */
141
+ export class SynapseContainer extends GenericContainer {
142
+ config;
143
+ mas;
144
+ constructor() {
145
+ super(`ghcr.io/element-hq/synapse:${TAG}`);
146
+ this.config = deepCopy(DEFAULT_CONFIG);
147
+ this.config.registration_shared_secret = randB64Bytes(16);
148
+ this.config.macaroon_secret_key = randB64Bytes(16);
149
+ this.config.form_secret = randB64Bytes(16);
150
+ const signingKey = randB64Bytes(32);
151
+ this.withWaitStrategy(Wait.forHttp("/health", 8008)).withCopyContentToContainer([
152
+ { target: this.config.signing_key_path, content: `ed25519 x ${signingKey}` },
153
+ {
154
+ target: this.config.log_config,
155
+ content: YAML.stringify({
156
+ version: 1,
157
+ formatters: {
158
+ precise: {
159
+ format: "%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s",
160
+ },
161
+ },
162
+ handlers: {
163
+ console: {
164
+ class: "logging.StreamHandler",
165
+ formatter: "precise",
166
+ },
167
+ },
168
+ loggers: {
169
+ "synapse.storage.SQL": {
170
+ level: "DEBUG",
171
+ },
172
+ "twisted": {
173
+ handlers: ["console"],
174
+ propagate: false,
175
+ },
176
+ },
177
+ root: {
178
+ level: "DEBUG",
179
+ handlers: ["console"],
180
+ },
181
+ disable_existing_loggers: false,
182
+ }),
183
+ },
184
+ ]);
185
+ }
186
+ withConfigField(key, value) {
187
+ set(this.config, key, value);
188
+ return this;
189
+ }
190
+ withConfig(config) {
191
+ this.config = {
192
+ ...this.config,
193
+ ...config,
194
+ };
195
+ return this;
196
+ }
197
+ withSmtpServer(mailpit) {
198
+ this.config.email = {
199
+ enable_notifs: false,
200
+ smtp_host: mailpit.internalHost,
201
+ smtp_port: mailpit.internalSmtpPort,
202
+ smtp_user: "username",
203
+ smtp_pass: "password",
204
+ require_transport_security: false,
205
+ notif_from: "Your Friendly %(app)s homeserver <noreply@example.com>",
206
+ app_name: "Matrix",
207
+ notif_template_html: "notif_mail.html",
208
+ notif_template_text: "notif_mail.txt",
209
+ notif_for_new_users: true,
210
+ client_base_url: "http://localhost/element",
211
+ };
212
+ return this;
213
+ }
214
+ withMatrixAuthenticationService(mas) {
215
+ this.mas = mas;
216
+ return this;
217
+ }
218
+ async start() {
219
+ // Synapse config public_baseurl needs to know what URL it'll be accessed from, so we have to map the port manually
220
+ const port = await getFreePort();
221
+ this.withExposedPorts({
222
+ container: 8008,
223
+ host: port,
224
+ })
225
+ .withConfig({
226
+ public_baseurl: `http://localhost:${port}`,
227
+ })
228
+ .withCopyContentToContainer([
229
+ {
230
+ target: "/data/homeserver.yaml",
231
+ content: YAML.stringify(this.config),
232
+ },
233
+ ]);
234
+ const container = await super.start();
235
+ const baseUrl = `http://localhost:${port}`;
236
+ if (this.mas) {
237
+ return new StartedSynapseWithMasContainer(container, baseUrl, this.config.registration_shared_secret, this.mas);
238
+ }
239
+ return new StartedSynapseContainer(container, baseUrl, this.config.registration_shared_secret);
240
+ }
241
+ }
242
+ /**
243
+ * A started Synapse testcontainer
244
+ */
245
+ export class StartedSynapseContainer extends AbstractStartedContainer {
246
+ baseUrl;
247
+ registrationSharedSecret;
248
+ adminTokenPromise;
249
+ adminApi;
250
+ csApi;
251
+ constructor(container, baseUrl, registrationSharedSecret) {
252
+ super(container);
253
+ this.baseUrl = baseUrl;
254
+ this.registrationSharedSecret = registrationSharedSecret;
255
+ this.adminApi = new Api(`${this.baseUrl}/_synapse/admin`);
256
+ this.csApi = new ClientServerApi(this.baseUrl);
257
+ }
258
+ /**
259
+ * Restart the container
260
+ * Useful to reset the state of the homeserver between tests
261
+ * @param options - options to pass to the restart
262
+ */
263
+ restart(options) {
264
+ this.adminTokenPromise = undefined;
265
+ return super.restart(options);
266
+ }
267
+ setRequest(request) {
268
+ this.csApi.setRequest(request);
269
+ this.adminApi.setRequest(request);
270
+ }
271
+ async onTestFinished(testInfo) {
272
+ // Clean up the server to prevent rooms leaking between tests
273
+ await this.deletePublicRooms();
274
+ }
275
+ async deletePublicRooms() {
276
+ const token = await this.getAdminToken();
277
+ // We hide the rooms from the room directory to save time between tests and for portability between homeservers
278
+ const { chunk: rooms } = await this.csApi.request("GET", "/v3/publicRooms", token, {});
279
+ await Promise.all(rooms.map((room) => this.csApi.request("PUT", `/v3/directory/list/room/${room.room_id}`, token, { visibility: "private" })));
280
+ }
281
+ async registerUserInternal(username, password, displayName, admin = false) {
282
+ const path = "/v1/register";
283
+ const { nonce } = await this.adminApi.request("GET", path, undefined, {});
284
+ const mac = crypto
285
+ .createHmac("sha1", this.registrationSharedSecret)
286
+ .update(`${nonce}\0${username}\0${password}\0${admin ? "" : "not"}admin`)
287
+ .digest("hex");
288
+ const data = await this.adminApi.request("POST", path, undefined, {
289
+ nonce,
290
+ username,
291
+ password,
292
+ mac,
293
+ admin,
294
+ displayname: displayName,
295
+ });
296
+ return {
297
+ homeServer: data.home_server || data.user_id.split(":").slice(1).join(":"),
298
+ accessToken: data.access_token,
299
+ userId: data.user_id,
300
+ deviceId: data.device_id,
301
+ password,
302
+ displayName,
303
+ username,
304
+ };
305
+ }
306
+ async getAdminToken() {
307
+ if (this.adminTokenPromise === undefined) {
308
+ this.adminTokenPromise = this.registerUserInternal("admin", "totalyinsecureadminpassword", undefined, true).then((res) => res.accessToken);
309
+ }
310
+ return this.adminTokenPromise;
311
+ }
312
+ async adminRequest(verb, path, data) {
313
+ const adminToken = await this.getAdminToken();
314
+ return this.adminApi.request(verb, path, adminToken, data);
315
+ }
316
+ /**
317
+ * Register a user on the given Homeserver using the shared registration secret.
318
+ * @param username - the username of the user to register
319
+ * @param password - the password of the user to register
320
+ * @param displayName - optional display name to set on the newly registered user
321
+ */
322
+ registerUser(username, password, displayName) {
323
+ return this.registerUserInternal(username, password, displayName, false);
324
+ }
325
+ /**
326
+ * Logs into synapse with the given username/password
327
+ * @param userId - login username
328
+ * @param password - login password
329
+ */
330
+ async loginUser(userId, password) {
331
+ return this.csApi.loginUser(userId, password);
332
+ }
333
+ /**
334
+ * Binds a 3pid
335
+ * @param userId - the username of the user to bind the 3pid to
336
+ * @param medium - the medium of the 3pid to bind
337
+ * @param address - the address of the 3pid to bind
338
+ */
339
+ async setThreepid(userId, medium, address) {
340
+ await this.adminRequest("PUT", `/v2/users/${userId}`, {
341
+ threepids: [
342
+ {
343
+ medium,
344
+ address,
345
+ },
346
+ ],
347
+ });
348
+ }
349
+ }
350
+ /**
351
+ * A started Synapse container when delegating auth to MAS
352
+ */
353
+ export class StartedSynapseWithMasContainer extends StartedSynapseContainer {
354
+ mas;
355
+ constructor(container, baseUrl, registrationSharedSecret, mas) {
356
+ super(container, baseUrl, registrationSharedSecret);
357
+ this.mas = mas;
358
+ }
359
+ async getAdminToken() {
360
+ if (this.adminTokenPromise === undefined) {
361
+ this.adminTokenPromise = this.mas.getAdminToken();
362
+ }
363
+ return this.adminTokenPromise;
364
+ }
365
+ /**
366
+ * Register a user on the given Homeserver using the shared registration secret.
367
+ * @param username - the username of the user to register
368
+ * @param password - the password of the user to register
369
+ * @param displayName - optional display name to set on the newly registered user
370
+ */
371
+ registerUser(username, password, displayName) {
372
+ return this.mas.registerUser(username, password, displayName);
373
+ }
374
+ /**
375
+ * Binds a 3pid
376
+ * @param userId - the username of the user to bind the 3pid to
377
+ * @param medium - the medium of the 3pid to bind
378
+ * @param address - the address of the 3pid to bind
379
+ */
380
+ async setThreepid(userId, medium, address) {
381
+ return this.mas.setThreepid(userId, medium, address);
382
+ }
383
+ }
@@ -0,0 +1,49 @@
1
+ import { type APIRequestContext } from "@playwright/test";
2
+ export type Verb = "GET" | "POST" | "PUT" | "DELETE";
3
+ /**
4
+ * A generic API client.
5
+ */
6
+ export declare class Api {
7
+ private readonly baseUrl;
8
+ private _request?;
9
+ constructor(baseUrl: string);
10
+ /**
11
+ * Set the request context to use for making requests.
12
+ * @param request - The request context to use.
13
+ */
14
+ setRequest(request: APIRequestContext): void;
15
+ /**
16
+ * Make a request to the API.
17
+ * @param verb - The HTTP verb to use.
18
+ * @param path - The path to request.
19
+ * @param token - The access token to use for the request.
20
+ * @param data - The data to send with the request.
21
+ */
22
+ request<R extends object>(verb: "GET", path: string, token?: string, data?: never): Promise<R>;
23
+ request<R extends object>(verb: Verb, path: string, token?: string, data?: object): Promise<R>;
24
+ }
25
+ /**
26
+ * Credentials for a user.
27
+ */
28
+ export interface Credentials {
29
+ accessToken: string;
30
+ userId: string;
31
+ deviceId: string;
32
+ homeServer: string;
33
+ password: string | null;
34
+ displayName?: string;
35
+ username: string;
36
+ }
37
+ /**
38
+ * A client-server API for interacting with a Matrix homeserver.
39
+ */
40
+ export declare class ClientServerApi extends Api {
41
+ constructor(baseUrl: string);
42
+ /**
43
+ * Register a user on the homeserver.
44
+ * @param userId - The user ID to register.
45
+ * @param password - The password to use for the user.
46
+ */
47
+ loginUser(userId: string, password: string): Promise<Credentials>;
48
+ }
49
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/utils/api.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,MAAM,IAAI,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAErD;;GAEG;AACH,qBAAa,GAAG;IAGO,OAAO,CAAC,QAAQ,CAAC,OAAO;IAF3C,OAAO,CAAC,QAAQ,CAAC,CAAoB;gBAED,OAAO,EAAE,MAAM;IAEnD;;;OAGG;IACI,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAInD;;;;;;OAMG;IACU,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC;IAC9F,OAAO,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;CAyB9G;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,GAAG;gBACjB,OAAO,EAAE,MAAM;IAIlC;;;;OAIG;IACU,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAwBjF"}
@@ -0,0 +1,73 @@
1
+ /*
2
+ Copyright 2025 New Vector Ltd.
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
+ Please see LICENSE files in the repository root for full details.
6
+ */
7
+ /**
8
+ * A generic API client.
9
+ */
10
+ export class Api {
11
+ baseUrl;
12
+ _request;
13
+ constructor(baseUrl) {
14
+ this.baseUrl = baseUrl;
15
+ }
16
+ /**
17
+ * Set the request context to use for making requests.
18
+ * @param request - The request context to use.
19
+ */
20
+ setRequest(request) {
21
+ this._request = request;
22
+ }
23
+ async request(verb, path, token, data) {
24
+ if (!this._request) {
25
+ throw new Error("No request context set");
26
+ }
27
+ const url = `${this.baseUrl}${path}`;
28
+ const res = await this._request.fetch(url, {
29
+ data,
30
+ method: verb,
31
+ headers: token
32
+ ? {
33
+ Authorization: `Bearer ${token}`,
34
+ }
35
+ : undefined,
36
+ });
37
+ if (!res.ok()) {
38
+ throw new Error(`Request to ${url} failed with status ${res.status()}: ${JSON.stringify(await res.json())}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ }
43
+ /**
44
+ * A client-server API for interacting with a Matrix homeserver.
45
+ */
46
+ export class ClientServerApi extends Api {
47
+ constructor(baseUrl) {
48
+ super(`${baseUrl}/_matrix/client`);
49
+ }
50
+ /**
51
+ * Register a user on the homeserver.
52
+ * @param userId - The user ID to register.
53
+ * @param password - The password to use for the user.
54
+ */
55
+ async loginUser(userId, password) {
56
+ const json = await this.request("POST", "/v3/login", undefined, {
57
+ type: "m.login.password",
58
+ identifier: {
59
+ type: "m.id.user",
60
+ user: userId,
61
+ },
62
+ password: password,
63
+ });
64
+ return {
65
+ password,
66
+ accessToken: json.access_token,
67
+ userId: json.user_id,
68
+ deviceId: json.device_id,
69
+ homeServer: json.home_server || json.user_id.split(":").slice(1).join(":"),
70
+ username: userId.slice(1).split(":")[0],
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,25 @@
1
+ import { type BrowserContext, type TestInfo } from "@playwright/test";
2
+ import { type Readable } from "node:stream";
3
+ /**
4
+ * A logger that captures console logs from pages and testcontainers.
5
+ */
6
+ export declare class Logger {
7
+ private pages;
8
+ private logs;
9
+ /**
10
+ * Get a consumer function that captures logs for a given container.
11
+ * @param container - the human-readable name of the container.
12
+ */
13
+ getConsumer(container: string): (stream: Readable) => void;
14
+ /**
15
+ * Hook to call when a test starts.
16
+ * @param context - the browser context of the test.
17
+ */
18
+ onTestStarted(context: BrowserContext): Promise<void>;
19
+ /**
20
+ * Hook to call when a test finishes.
21
+ * @param testInfo - the info about the test that just finished.
22
+ */
23
+ onTestFinished(testInfo: TestInfo): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,cAAc,EAAa,KAAK,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjF,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C;;GAEG;AACH,qBAAa,MAAM;IACf,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,IAAI,CAA8B;IAE1C;;;OAGG;IACI,WAAW,CAAC,SAAS,EAAE,MAAM,IAExB,QAAQ,QAAQ;IAU5B;;;OAGG;IACU,aAAa,CAAC,OAAO,EAAE,cAAc;IAyBlD;;;OAGG;IACU,cAAc,CAAC,QAAQ,EAAE,QAAQ;CAWjD"}
@@ -0,0 +1,74 @@
1
+ /*
2
+ Copyright 2024-2025 New Vector Ltd.
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5
+ Please see LICENSE files in the repository root for full details.
6
+ */
7
+ import stripAnsi from "strip-ansi";
8
+ /**
9
+ * A logger that captures console logs from pages and testcontainers.
10
+ */
11
+ export class Logger {
12
+ pages = [];
13
+ logs = {};
14
+ /**
15
+ * Get a consumer function that captures logs for a given container.
16
+ * @param container - the human-readable name of the container.
17
+ */
18
+ getConsumer(container) {
19
+ this.logs[container] = "";
20
+ return (stream) => {
21
+ stream.on("data", (chunk) => {
22
+ this.logs[container] += chunk.toString();
23
+ });
24
+ stream.on("err", (chunk) => {
25
+ this.logs[container] += "ERR " + chunk.toString();
26
+ });
27
+ };
28
+ }
29
+ /**
30
+ * Hook to call when a test starts.
31
+ * @param context - the browser context of the test.
32
+ */
33
+ async onTestStarted(context) {
34
+ this.pages = [];
35
+ for (const id in this.logs) {
36
+ if (id.startsWith("page-")) {
37
+ delete this.logs[id];
38
+ }
39
+ else {
40
+ this.logs[id] = "";
41
+ }
42
+ }
43
+ context.on("console", (msg) => {
44
+ const page = msg.page();
45
+ if (!page)
46
+ return;
47
+ let pageIdx = this.pages.indexOf(page);
48
+ if (pageIdx === -1) {
49
+ this.pages.push(page);
50
+ pageIdx = this.pages.length - 1;
51
+ this.logs[`page-${pageIdx}`] = `Console logs for page with URL: ${page.url()}\n\n`;
52
+ }
53
+ const type = msg.type();
54
+ const text = msg.text();
55
+ this.logs[`page-${pageIdx}`] += `${type}: ${text}\n`;
56
+ });
57
+ }
58
+ /**
59
+ * Hook to call when a test finishes.
60
+ * @param testInfo - the info about the test that just finished.
61
+ */
62
+ async onTestFinished(testInfo) {
63
+ if (testInfo.status !== "passed") {
64
+ for (const id in this.logs) {
65
+ if (!this.logs[id])
66
+ continue;
67
+ await testInfo.attach(id, {
68
+ body: stripAnsi(this.logs[id]),
69
+ contentType: "text/plain",
70
+ });
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Deep copy the given object. The object MUST NOT have circular references and
3
+ * MUST NOT have functions.
4
+ * @param obj - The object to deep copy.
5
+ * @returns A copy of the object without any references to the original.
6
+ */
7
+ export declare function deepCopy<T>(obj: T): T;
8
+ //# sourceMappingURL=object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object.d.ts","sourceRoot":"","sources":["../../src/utils/object.ts"],"names":[],"mappings":"AAOA;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAErC"}
@@ -0,0 +1,15 @@
1
+ /*
2
+ Copyright 2024-2025 New Vector Ltd.
3
+
4
+ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
5
+ Please see LICENSE files in the repository root for full details.
6
+ */
7
+ /**
8
+ * Deep copy the given object. The object MUST NOT have circular references and
9
+ * MUST NOT have functions.
10
+ * @param obj - The object to deep copy.
11
+ * @returns A copy of the object without any references to the original.
12
+ */
13
+ export function deepCopy(obj) {
14
+ return JSON.parse(JSON.stringify(obj));
15
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Get a free networking port on the system.
3
+ */
4
+ export declare function getFreePort(): Promise<number>;
5
+ //# sourceMappingURL=port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/utils/port.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAQnD"}
@@ -0,0 +1,20 @@
1
+ /*
2
+ Copyright 2024-2025 New Vector Ltd.
3
+ Copyright 2022 The Matrix.org Foundation C.I.C.
4
+
5
+ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6
+ Please see LICENSE files in the repository root for full details.
7
+ */
8
+ import * as net from "node:net";
9
+ /**
10
+ * Get a free networking port on the system.
11
+ */
12
+ export async function getFreePort() {
13
+ return new Promise((resolve) => {
14
+ const srv = net.createServer();
15
+ srv.listen(0, () => {
16
+ const port = srv.address().port;
17
+ srv.close(() => resolve(port));
18
+ });
19
+ });
20
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generate a random base64 string of the given number of bytes.
3
+ * @param numBytes - The number of bytes to generate.
4
+ */
5
+ export declare function randB64Bytes(numBytes: number): string;
6
+ //# sourceMappingURL=rand.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rand.d.ts","sourceRoot":"","sources":["../../src/utils/rand.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD"}